There are many ways the code may fail, if another process (including shell used by a human) interferes with the files or their directories at the same time.
If we ignore all the common reasons (interference from another process, or the entire filesystem being unmounted), there is one detail in XFS that can affect this - assuming it is a 32-bit binary: the inode number for the XFS file could exceed 232-1 = 4,294,967,295 causing fs/stat.c:cp_old_stat() or fs/stat.c:cp_new_stat() to fail with EOVERFLOW.
To verify, check the original binary with file . If it is 64-bit, the inode number cannot be the reason. If it is 32-bit, then the inode number is a possible culprit. To fix, recompile the binary to 64-bit.
The code indeed is suspect, though.
The problem is relying on readdir() while modifying the contents of said directory. Because of how newer filesystems work, there is no guarantee of readdir() seeing only new files, and not seeing files already moved.
The proper approach is to obtain the full list of files first – you can use e.g. scandir(), glob(), or nftw(); scandir() or glob() if you want to process the files as a set, nftw() if you move each file in the callback function. You see, both glob() and nftw() are supposed to handle the "directory may change during walk" correctly, even if the underlying readdir() does not.
(There is also the fts family of traversing filesystem trees, but on Linux, the glibc implementation prior to 2.23 (released in February, 2016) was not safe with 64-bit file offsets.)
Consider this implementation:
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#define _ATFILE_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <ftw.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
/* Because file_mover() does not descend into subdirectories, it needs a
very small number of descriptors; four should always suffice. */
#define FILE_MOVER_FDS 4
/* Per-thread state for file_mover(). Each thread sees separate variables! */
static __thread int file_mover_destfd = -1;
static __thread int file_mover_errno = 0;
static __thread long file_mover_count = 0;
static int file_mover(const char *srcpath, const struct stat *srcinfo, int typeflag, struct FTW *details)
{
/* Initial argument (srcpath)? */
if (details->level == 0) {
/* If it specifies a directory, move all its files. */
if (typeflag == FTW_D)
return FTW_CONTINUE;
/* Otherwise fail. */
file_mover_errno = ENODEV;
return FTW_STOP;
}
/* Skip directories. */
if (typeflag == FTW_D || typeflag == FTW_DNR || typeflag == FTW_DP)
return FTW_SKIP_SUBTREE;
/* Ignore all but ordinary files. */
if (typeflag != FTW_F)
return FTW_CONTINUE;
/* Obtain file name part, and its length. */
const char *name = srcpath + details->base;
const size_t namelen = strlen(name);
/* Zero-length files should never occur; detect them anyway. */
if (namelen < 1) {
file_mover_errno = ENOENT;
return FTW_STOP;
}
/* Detect if destination descriptor is invalid. */
if (file_mover_destfd == -1) {
file_mover_errno = EBADF;
return FTW_STOP;
}
/* Source path is either absolute, or relative to current working directory. */
if (renameat(AT_FDCWD, srcpath, file_mover_destfd, name) == -1) {
file_mover_errno = errno;
return FTW_STOP;
}
/* Verify the target file exists and matches the original file. */
{
struct stat destinfo;
if (fstatat(file_mover_destfd, name, &destinfo, 0) == -1) {
file_mover_errno = errno;
return FTW_STOP;
}
/* Size and mode matches? */
if (destinfo.st_size != srcinfo->st_size || destinfo.st_mode != srcinfo->st_mode) {
file_mover_errno = EIO;
return FTW_STOP;
}
}
/* Add to the running count. */
file_mover_count++;
return FTW_CONTINUE;
}
/* Move files from directory srcdir to directory destdir.
Returns -1 if an error occurs with errno set to indicate the error,
and the number of files moved otherwise.
*/
static long move_files(const char *srcdir, const char *destdir)
{
/* Paranoid sanity checks. */
if (!srcdir || !destdir) {
errno = EINVAL;
return -1;
}
/* Open the destination directory as a handle. */
do {
file_mover_destfd = open(destdir, O_PATH | O_CLOEXEC);
} while (file_mover_destfd == -1 && errno == EINTR);
if (file_mover_destfd == -1) {
return -1;
}
file_mover_errno = 0;
file_mover_count = 0;
if (nftw(srcdir, file_mover, FILE_MOVER_FDS, FTW_ACTIONRETVAL) != 0) {
/* Failed. Return reason in errno. */
close(file_mover_destfd);
file_mover_destfd = -1;
errno = file_mover_errno;
return -1;
}
if (close(file_mover_destfd) == -1) {
file_mover_destfd = -1;
/* errno set by close() */
return -1;
}
file_mover_destfd = -1;
/* Success. (Note: technically, the count could overflow on 32-bit arches.) */
return file_mover_count;
}
int main(int argc, char *argv[])
{
long n;
if (argc != 3) {
const char *cmd = (argc > 0 && argv[0] && argv[0][0]) ? argv[0] : "(this)";
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help]\n", cmd);
fprintf(stderr, " %s SOURCE-DIRECTORY DESTINATION-DIRECTORY\n", cmd);
fprintf(stderr, "\n");
if (argc == 2 && (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")))
return EXIT_SUCCESS;
else
return EXIT_FAILURE;
}
n = move_files(argv[1], argv[2]);
if (n < 0) {
fprintf(stderr, "Failed: %s (%d)\n", strerror(errno), errno);
return EXIT_FAILURE;
} else
if (!n) {
fprintf(stderr, "No files to move.\n");
return EXIT_SUCCESS;
}
if (n == 1)
printf("1 file moved.\n");
else
printf("%ld files moved.\n", n);
return EXIT_SUCCESS;
}
To run it in an endless loop, use
int main(int argc, char *argv[])
{
long n, expected = 0;
if (argc != 3) {
const char *cmd = (argc > 0 && argv[0] && argv[0][0]) ? argv[0] : "(this)";
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help]\n", cmd);
fprintf(stderr, " %s SOURCE-DIRECTORY DESTINATION-DIRECTORY\n", cmd);
fprintf(stderr, "\n");
if (argc == 2 && (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")))
return EXIT_SUCCESS;
else
return EXIT_FAILURE;
}
while(1) {
n = move_files(argv[1], argv[2]);
if (n < 0) {
fprintf(stderr, "Failed: %s (%d)\n", strerror(errno), errno);
return EXIT_FAILURE;
} else
if (!n) {
fprintf(stderr, "No files to move.\n");
return EXIT_SUCCESS;
}
if (!expected) {
expected = n;
fprintf(stderr, "Moving %ld files around.\n", n);
} else
if (n != expected) {
fprintf(stderr, "Moved %ld of %ld files!\n", n, expected);
return EXIT_FAILURE;
}
n = move_files(argv[2], argv[1]);
if (n < 0) {
fprintf(stderr, "Failed: %s (%d)\n", strerror(errno), errno);
return EXIT_FAILURE;
} else
if (n != expected) {
fprintf(stderr, "Moved only %ld of %ld files!\n", n, expected);
return EXIT_FAILURE;
}
}
return EXIT_SUCCESS;
}
which will detect if you e.g. delete one of the files during its testing.