Make rsync() recreate symlinks only on request

Fixing LITE_FOPT_COPYFILE_SYM in copyfile() also changed rsync(), which
has always passed that flag.  Every caller would start getting links in
the destination where they used to get a copy of the file.

Keep following links by default and add LITE_FOPT_RSYNC_LINKS for
callers that want them recreated.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg committed 2026-11-14 12:46:48 +01:00
1 parent 666a8ef7cd
commit 4609c53ddd
5 files changed
+42 -12

No files matched your search

+7
View File
@@ -7,7 +7,14 @@ All notable changes to the project are documented in this file.
[UNRELEASED][] [UNRELEASED][]
-------------- --------------
### Changes
- Add `LITE_FOPT_RSYNC_LINKS` option to `rsync()`, recreates symlinks to
files in the destination instead of copying what they point to
### Fixes ### Fixes
- Fix #23: `copyfile()` ignored `LITE_FOPT_COPYFILE_SYM` and always
copied what a symlink points to. It now recreates the symlink, also
when it is dangling or points to a directory
- Fix `copyfile()` not preserving the source file mode when the process - Fix `copyfile()` not preserving the source file mode when the process
umask is more restrictive umask is more restrictive
- Fix `mkpath()`, and `fmkpath()`, not applying the given mode when the - Fix `mkpath()`, and `fmkpath()`, not applying the given mode when the
+6
View File
@@ -244,6 +244,12 @@ Generic Functions
- `LITE_FOPT_RSYNC_DELETE (0x01)` - `LITE_FOPT_RSYNC_DELETE (0x01)`
- `LITE_FOPT_KEEP_MTIME (0x02)` - `LITE_FOPT_KEEP_MTIME (0x02)`
- `LITE_FOPT_RSYNC_LINKS (0x04)`
Symlinks in the source tree are followed by default, the destination
gets a copy of what they point to. With `LITE_FOPT_RSYNC_LINKS` a
symlink to a file, or a dangling symlink, is recreated in the
destination instead. Symlinks to directories are always followed.
In releases prior to v2.0 the argument controlling pruning was called In releases prior to v2.0 the argument controlling pruning was called
`delete`, it is now called `opt`. The APIs are 100% compatible if the `delete`, it is now called `opt`. The APIs are 100% compatible if the
+1
View File
@@ -67,6 +67,7 @@ extern "C"
#define LITE_FOPT_RSYNC_DELETE 0x01 /**< Prune files from destination that are not in source */ #define LITE_FOPT_RSYNC_DELETE 0x01 /**< Prune files from destination that are not in source */
#define LITE_FOPT_COPYFILE_SYM 0x01 /**< Recreate symlink or follow to copy target */ #define LITE_FOPT_COPYFILE_SYM 0x01 /**< Recreate symlink or follow to copy target */
#define LITE_FOPT_KEEP_MTIME 0x02 /**< Preserve modification time */ #define LITE_FOPT_KEEP_MTIME 0x02 /**< Preserve modification time */
#define LITE_FOPT_RSYNC_LINKS 0x04 /**< Recreate symlinks to files instead of copying their target */
typedef struct lfile lfile_t; /**< Opqaue context struct for lfile APIs */ typedef struct lfile lfile_t; /**< Opqaue context struct for lfile APIs */
typedef struct sdbuf sdbuf_t; /**< Opqaue context struct for telnet APIs */ typedef struct sdbuf sdbuf_t; /**< Opqaue context struct for telnet APIs */
+17 -5
View File
@@ -45,7 +45,8 @@ static int set_mtime(char *fn, struct stat *st);
* Synchronize contents and optionally remove non-existing backups * Synchronize contents and optionally remove non-existing backups
* @param src Source directory * @param src Source directory
* @param dst Destination directory * @param dst Destination directory
* @param opt An option mask of ::LITE_FOPT_RSYNC_DELETE, ::LITE_FOPT_KEEP_MTIME * @param opt An option mask of ::LITE_FOPT_RSYNC_DELETE, ::LITE_FOPT_KEEP_MTIME,
* ::LITE_FOPT_RSYNC_LINKS
* @param filter Optional filtering function for source directory. * @param filter Optional filtering function for source directory.
* *
* This is a miniature implementation of the famous rsync for local use * This is a miniature implementation of the famous rsync for local use
@@ -60,6 +61,13 @@ static int set_mtime(char *fn, struct stat *st);
* *
* %LITE_FOPT_RSYNC_DELETE: Prune files from @p dst that no longer exist in @p src. * %LITE_FOPT_RSYNC_DELETE: Prune files from @p dst that no longer exist in @p src.
* %LITE_FOPT_KEEP_MTIME: Preserve modification time * %LITE_FOPT_KEEP_MTIME: Preserve modification time
* %LITE_FOPT_RSYNC_LINKS: Recreate symlinks to files in @p dst
*
* Symlinks in @p src are followed by default, so @p dst gets a copy of
* what they point to. With ::LITE_FOPT_RSYNC_LINKS a symlink to a file,
* or a dangling symlink, is recreated in @p dst with the same target
* text, replacing any file or symlink already there. Symlinks to
* directories in @p src are always followed.
* *
* The filter callback, @p filter, if provided, is used to determine * The filter callback, @p filter, if provided, is used to determine
* what files to include from the source directory when backing up. If * what files to include from the source directory when backing up. If
@@ -87,7 +95,7 @@ int rsync(char *src, char *dst, int opt, int (*filter)(const char *file))
if (!fexist(src)) if (!fexist(src))
return 1; return 1;
if (copy(src, dst, keep_mtim)) if (copy(src, dst, opt))
return 1; return 1;
return 0; return 0;
@@ -137,7 +145,7 @@ int rsync(char *src, char *dst, int opt, int (*filter)(const char *file))
continue; /* Next file/dir in @src to copy... */ continue; /* Next file/dir in @src to copy... */
} }
if (copy(source, dst, keep_mtim)) if (copy(source, dst, opt))
result++; result++;
} }
@@ -162,8 +170,12 @@ static int copy(char *src, char *dst, int opt)
{ {
char *dest = NULL, *temp = NULL, *file = NULL; char *dest = NULL, *temp = NULL, *file = NULL;
int result = 1, saved_errno, made_temp = 0; int result = 1, saved_errno, made_temp = 0;
int copt = opt & LITE_FOPT_KEEP_MTIME;
copyfile(src, dst, 0, opt | LITE_FOPT_COPYFILE_SYM); if (opt & LITE_FOPT_RSYNC_LINKS)
copt |= LITE_FOPT_COPYFILE_SYM;
copyfile(src, dst, 0, copt);
if (errno != EEXIST) if (errno != EEXIST)
return errno ? 1 : 0; return errno ? 1 : 0;
@@ -186,7 +198,7 @@ static int copy(char *src, char *dst, int opt)
if (asprintf(&file, "%s/link", temp) < 0) if (asprintf(&file, "%s/link", temp) < 0)
goto exit; goto exit;
copyfile(src, file, 0, opt | LITE_FOPT_COPYFILE_SYM); copyfile(src, file, 0, copt);
if (errno) if (errno)
goto exit; goto exit;
result = rename(file, dest) != 0; result = rename(file, dest) != 0;
+11 -7
View File
@@ -65,29 +65,33 @@ int main(void)
check_link("copied", "file"); check_link("copied", "file");
fail_unless(lstat("copied", &dst_st) == 0 && dst_st.st_mtime == times[1].tv_sec); fail_unless(lstat("copied", &dst_st) == 0 && dst_st.st_mtime == times[1].tv_sec);
fail_unless(unlink("copied") == 0); fail_unless(unlink("copied") == 0);
/* Exercise the existing rsync file caller, which requests SYM. */ /* rsync() follows links unless asked to recreate them. */
fail_unless(rsync("link", "copied", 0, NULL) == 0); fail_unless(rsync("link", "copied", 0, NULL) == 0);
fail_unless(lstat("copied", &dst_st) == 0 && S_ISREG(dst_st.st_mode));
fail_unless(dst_st.st_size == sizeof(payload));
fail_unless(unlink("copied") == 0);
fail_unless(rsync("link", "copied", LITE_FOPT_RSYNC_LINKS, NULL) == 0);
check_link("copied", "file"); check_link("copied", "file");
/* Reuse the destination after changing the source link. */ /* Reuse the destination after changing the source link. */
fail_unless(unlink("link") == 0 && symlink("file", "link") == 0); fail_unless(unlink("link") == 0 && symlink("file", "link") == 0);
fail_unless(rsync("link", "out", 0, NULL) == 0); fail_unless(rsync("link", "out", LITE_FOPT_RSYNC_LINKS, NULL) == 0);
check_link("out/link", "file"); check_link("out/link", "file");
fail_unless(unlink("link") == 0 && symlink("copied", "link") == 0); fail_unless(unlink("link") == 0 && symlink("copied", "link") == 0);
fail_unless(rsync("link", "out", 0, NULL) == 0); fail_unless(rsync("link", "out", LITE_FOPT_RSYNC_LINKS, NULL) == 0);
check_link("out/link", "copied"); check_link("out/link", "copied");
/* A same-path copy must not unlink its source. */ /* A same-path copy must not unlink its source. */
fail_unless(rsync("link", "link", 0, NULL) == 0); fail_unless(rsync("link", "link", LITE_FOPT_RSYNC_LINKS, NULL) == 0);
check_link("link", "copied"); check_link("link", "copied");
fail_unless(unlink("out/link") == 0); fail_unless(unlink("out/link") == 0);
/* Replacing a regular destination must also retain the source link. */ /* Replacing a regular destination must also retain the source link. */
fail_unless(touch("out/link") == 0); fail_unless(touch("out/link") == 0);
fail_unless(rsync("link", "out", 0, NULL) == 0); fail_unless(rsync("link", "out", LITE_FOPT_RSYNC_LINKS, NULL) == 0);
check_link("out/link", "copied"); check_link("out/link", "copied");
fail_unless(unlink("out/link") == 0); fail_unless(unlink("out/link") == 0);
/* A failed replacement must keep both the destination and source. */ /* A failed replacement must keep both the destination and source. */
fail_unless(mkdir("out/link", 0700) == 0); fail_unless(mkdir("out/link", 0700) == 0);
fail_unless(touch("out/link/keep") == 0); fail_unless(touch("out/link/keep") == 0);
fail_unless(rsync("link", "out", 0, NULL) != 0); fail_unless(rsync("link", "out", LITE_FOPT_RSYNC_LINKS, NULL) != 0);
fail_unless(fexist("out/link/keep")); fail_unless(fexist("out/link/keep"));
check_link("link", "copied"); check_link("link", "copied");
fail_unless(unlink("out/link/keep") == 0 && rmdir("out/link") == 0); fail_unless(unlink("out/link/keep") == 0 && rmdir("out/link") == 0);
@@ -95,7 +99,7 @@ int main(void)
fail_unless(mkdir("protected", 0700) == 0); fail_unless(mkdir("protected", 0700) == 0);
fail_unless(symlink("original", "protected/link") == 0); fail_unless(symlink("original", "protected/link") == 0);
fail_unless(chmod("protected", 0500) == 0); fail_unless(chmod("protected", 0500) == 0);
fail_unless(rsync("link", "protected", 0, NULL) != 0); fail_unless(rsync("link", "protected", LITE_FOPT_RSYNC_LINKS, NULL) != 0);
check_link("protected/link", "original"); check_link("protected/link", "original");
check_link("link", "copied"); check_link("link", "copied");
fail_unless(chmod("protected", 0700) == 0); fail_unless(chmod("protected", 0700) == 0);