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:
M | cmd/mv.c | 171 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------- |
M | test-cmd/mv.t | 15 | +++++++++++++++ |
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 .
.