logo

utils-std

Collection of commonly available Unix tools
commit: 1c46b8aecfd0489eab59f53e157700c45323776d
parent d29e712c7db44455f002fe27e1d83610a25c70ec
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date:   Wed, 31 Jul 2024 13:24:32 +0200

cmd/mv: Move directory contents accross filesystems

Diffstat:

Mcmd/mv.c171++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mtest-cmd/mv.t15+++++++++++++++
2 files changed, 162 insertions(+), 24 deletions(-)

diff --git a/cmd/mv.c b/cmd/mv.c @@ -6,10 +6,16 @@ #define _GNU_SOURCE // copy_file_range #define _FILE_OFFSET_BITS 64 +// NetBSD <10 hides fdopendir behind _NETBSD_SOURCE +#if __NetBSD_Version__ < 1000000000 +#define _NETBSD_SOURCE +#endif + #include "../lib/consent.h" #include "../lib/fs.h" #include <assert.h> +#include <dirent.h> // fdopendir #include <errno.h> #include <fcntl.h> // open #include <libgen.h> // basename @@ -41,39 +47,102 @@ struct named_fd char *name; }; +static int do_renameat(struct named_fd srcdir, + const char *restrict src, + struct named_fd destdir, + const char *restrict dest); + static int -copy_unlink(const char *restrict src, const char *restrict dest) +copy_file_unlink(struct named_fd srcdir, + const char *restrict src, + struct stat src_status, + struct named_fd destdir, + const char *restrict dest) { - int in = open(src, O_RDONLY | O_NOCTTY); + int in = openat(srcdir.fd, src, O_RDONLY | O_NOCTTY); if(in < 0) { - fprintf(stderr, "mv: Failed opening '%s': %s\n", src, strerror(errno)); + fprintf(stderr, "mv: Failed opening '%s/%s': %s\n", srcdir.name, src, strerror(errno)); errno = 0; return -1; } - int out = open(dest, O_WRONLY | O_CREAT | O_NOCTTY); + int out = openat(destdir.fd, dest, O_WRONLY | O_CREAT | O_NOCTTY); if(out < 0) { - fprintf(stderr, "mv: Failed opening '%s': %s\n", dest, strerror(errno)); + fprintf(stderr, "mv: Failed opening '%s/%s': %s\n", destdir.name, dest, strerror(errno)); errno = 0; return -1; } - if(auto_file_copy(in, out, SIZE_MAX, 0) < 0) return -1; + if(auto_file_copy(in, out, src_status.st_size, 0) < 0) return -1; - return unlink(src); + return unlinkat(srcdir.fd, src, 0); } static int -do_renameat(const char *restrict src, struct named_fd destdir, const char *restrict dest) +rename_dir_entries(struct named_fd srcdir, struct named_fd destdir) { - if(destdir.fd == AT_FDCWD && strcmp(src, dest) == 0) + DIR *dirsrc = fdopendir(srcdir.fd); + if(dirsrc == NULL) + { + fprintf( + stderr, "mv: Failed fd-opening source directory '%s': %s\n", srcdir.name, strerror(errno)); + return -1; + } + + while(true) + { + assert(errno == 0); + errno = 0; + struct dirent *dirsrc_ent = readdir(dirsrc); + if(dirsrc_ent == NULL) + { + if(errno == 0) break; + + fprintf( + stderr, "mv: Failed reading source directory '%s': %s\n", srcdir.name, strerror(errno)); + closedir(dirsrc); + errno = 0; + return -1; + } + + if(strcmp(dirsrc_ent->d_name, ".") == 0) continue; + if(strcmp(dirsrc_ent->d_name, "..") == 0) continue; + + if(do_renameat(srcdir, dirsrc_ent->d_name, destdir, dirsrc_ent->d_name) < 0) + { + closedir(dirsrc); + return -1; + } + } + + return 0; +} + +static int +do_renameat(struct named_fd srcdir, + const char *restrict src, + struct named_fd destdir, + const char *restrict dest) +{ + if(destdir.fd == srcdir.fd && strcmp(src, dest) == 0) { fprintf(stderr, "mv: Error, passed to both source and destination: '%s'\n", src); return -1; } + struct stat src_status; + if(fstatat(srcdir.fd, src, &src_status, 0) < 0) + { + fprintf(stderr, + "mv: Failed getting status for source file '%s/%s': %s\n", + srcdir.name, + src, + strerror(errno)); + return -1; + } + errno = 0; struct stat dest_status; @@ -87,17 +156,11 @@ do_renameat(const char *restrict src, struct named_fd destdir, const char *restr strerror(errno)); return -1; } + errno = 0; if(ret == 0) { - struct stat src_status; - if(fstatat(AT_FDCWD, src, &src_status, 0) < 0) - { - fprintf(stderr, "mv: Failed getting status for source file '%s': %s\n", src, strerror(errno)); - return -1; - } - if(dest_status.st_ino == src_status.st_ino && dest_status.st_dev == src_status.st_dev) return 0; if(no_clob) @@ -132,13 +195,69 @@ do_renameat(const char *restrict src, struct named_fd destdir, const char *restr } assert(errno == 0); - if(renameat(AT_FDCWD, src, destdir.fd, dest) < 0) + if(renameat(srcdir.fd, src, destdir.fd, dest) < 0) { switch(errno) { case EXDEV: errno = 0; - if(copy_unlink(src, dest) < 0) return -1; + + if(S_ISDIR(src_status.st_mode)) + { + char child_srcdir_name[PATH_MAX] = ""; + snprintf(child_srcdir_name, PATH_MAX, "%s/%s", srcdir.name, src); + + struct named_fd child_srcdir = { + .fd = openat(srcdir.fd, src, O_RDONLY | O_DIRECTORY | O_CLOEXEC), + .name = child_srcdir_name, + }; + + if(child_srcdir.fd < 0) + { + fprintf(stderr, + "mv: Failed opening source directory '%s/%s': %s\n", + srcdir.name, + src, + strerror(errno)); + return -1; + } + + if(mkdirat(destdir.fd, dest, src_status.st_mode) < 0) + { + fprintf(stderr, + "mv: Failed creating destination directory '%s/%s': %s\n", + destdir.name, + dest, + strerror(errno)); + return -1; + } + + char child_destdir_name[PATH_MAX] = ""; + snprintf(child_destdir_name, PATH_MAX, "%s/%s", destdir.name, dest); + struct named_fd child_destdir = { + .fd = openat(destdir.fd, dest, O_RDONLY | O_DIRECTORY | O_CLOEXEC), + .name = child_destdir_name, + }; + + if(rename_dir_entries(child_srcdir, child_destdir) < 0) return -1; + + close(child_srcdir.fd); + close(child_destdir.fd); + + if(unlinkat(srcdir.fd, src, AT_REMOVEDIR) < 0) + { + fprintf(stderr, + "mv: Failed removing source directory '%s/%s': %s\n", + srcdir.name, + src, + strerror(errno)); + return -1; + } + } + else + { + if(copy_file_unlink(srcdir, src, src_status, destdir, dest) < 0) return -1; + } break; case EISDIR: case ENOTDIR: @@ -148,7 +267,7 @@ do_renameat(const char *restrict src, struct named_fd destdir, const char *restr return -1; } - int tmp_destdir = open(dest, O_SEARCH | O_DIRECTORY); + int tmp_destdir = openat(destdir.fd, dest, O_RDONLY | O_DIRECTORY | O_CLOEXEC); if(tmp_destdir < 0) { fprintf( @@ -156,7 +275,7 @@ do_renameat(const char *restrict src, struct named_fd destdir, const char *restr return -1; } - if(renameat(AT_FDCWD, src, tmp_destdir, src) < 0) + if(renameat(srcdir.fd, src, tmp_destdir, src) < 0) { fprintf( stderr, "mv: Failed moving '%s' into directory '%s': %s\n", src, dest, strerror(errno)); @@ -200,6 +319,10 @@ main(int argc, char *argv[]) .fd = AT_FDCWD, .name = ".", }; + struct named_fd srcdir = { + .fd = AT_FDCWD, + .name = ".", + }; int c = -1; while((c = getopt(argc, argv, ":fint:v")) != -1) @@ -223,7 +346,7 @@ main(int argc, char *argv[]) break; case 't': destdir.name = optarg; - destdir.fd = open(optarg, O_SEARCH | O_DIRECTORY); + destdir.fd = open(optarg, O_RDONLY | O_DIRECTORY | O_CLOEXEC); if(destdir.fd < 0) { @@ -274,7 +397,7 @@ main(int argc, char *argv[]) int ret_stat = fstatat(destdir.fd, argv[1], &dest_status, 0); if(argc == 2 && (errno == ENOENT || (ret_stat == 0 && !S_ISDIR(dest_status.st_mode)))) { - int ret = do_renameat(argv[0], destdir, argv[1]); + int ret = do_renameat(srcdir, argv[0], destdir, argv[1]); consent_finish(); return ret < 0 ? 1 : 0; @@ -283,7 +406,7 @@ main(int argc, char *argv[]) argc--; destdir.name = argv[argc]; - destdir.fd = open(destdir.name, O_SEARCH | O_DIRECTORY); + destdir.fd = open(destdir.name, O_RDONLY | O_DIRECTORY | O_CLOEXEC); if(destdir.fd < 0) { fprintf(stderr, @@ -303,7 +426,7 @@ main(int argc, char *argv[]) char *filename = basename(arg); - if(do_renameat(argv[i], destdir, filename) < 0) + if(do_renameat(srcdir, argv[i], destdir, filename) < 0) { consent_finish(); return 1; diff --git a/test-cmd/mv.t b/test-cmd/mv.t @@ -135,6 +135,21 @@ Last component used for destination filename $ test -f dest_last/file $ rm -r src_last dest_last +Successfully moves directory contents accross filesystems +(Assuming /dev/shm is a separated fs, as it should be when present) + $ mkdir non-empty-dir + $ touch non-empty-dir/foo + $ mkdir non-empty-dir/foo.d + $ touch non-empty-dir/foo.d/bar + $ test -f non-empty-dir/foo + $ test -f non-empty-dir/foo.d/bar + $ rm -f /dev/shm/mv-test-non-empty-dir + $ mv non-empty-dir /dev/shm/mv-test-non-empty-dir + $ test -f /dev/shm/mv-test-non-empty-dir/foo + $ test -f /dev/shm/mv-test-non-empty-dir/foo.d/bar + $ test ! -e non-empty-dir + $ rm -fr /dev/shm/mv-test-non-empty-dir + No files should be left $ find . .