diff --git a/.github/workflows/build-publish.yml b/.github/workflows/build-publish.yml new file mode 100644 index 000000000..f8d6233d3 --- /dev/null +++ b/.github/workflows/build-publish.yml @@ -0,0 +1,27 @@ +name: Build & publish +on: {push: {branches: [master]}, pull_request: {branches: [master]}, workflow_dispatch} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: '0' + - id: debianise + uses: twojstaryzdomu/debianise@HEAD + with: + create_changelog: true + install_build_depends: false + debug: true + - id: action-gh-release + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + files: ${{ steps.debianise.outputs.files }} + name: ${{ steps.debianise.outputs.release_name }} + tag_name: ${{ steps.debianise.outputs.tag_name }} + fail_on_unmatched_files: true + draft: true + prerelease: true diff --git a/debian/compat b/debian/compat new file mode 100644 index 000000000..b4de39476 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +11 diff --git a/debian/control b/debian/control new file mode 100644 index 000000000..39d8275bc --- /dev/null +++ b/debian/control @@ -0,0 +1,16 @@ +Source: rsync +Section: unknown +Priority: optional +Maintainer: unknown +Build-Depends: debhelper (>= 11) +Standards-Version: 4.1.3 +Homepage: https://github.com/twojstaryzdomu/rsync +Vcs-Git: https://github.com/twojstaryzdomu/rsync.git +Vcs-Browser: https://github.com/twojstaryzdomu/rsync + + +Package: rsync +Section: unknown +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends} +Description: rsync description diff --git a/debian/patches/disable_reconfigure_req.diff b/debian/patches/disable_reconfigure_req.diff new file mode 100644 index 000000000..1dce71069 --- /dev/null +++ b/debian/patches/disable_reconfigure_req.diff @@ -0,0 +1,38 @@ +diff --git a/Makefile.in b/Makefile.in +index 3cde955..ef71d7e 100644 +--- a/Makefile.in ++++ b/Makefile.in +@@ -210,15 +210,6 @@ configure.sh config.h.in: configure.ac aclocal.m4 + else \ + echo "config.h.in has CHANGED."; \ + fi +- @if test -f configure.sh.old || test -f config.h.in.old; then \ +- if test "$(MAKECMDGOALS)" = reconfigure; then \ +- echo 'Continuing with "make reconfigure".'; \ +- else \ +- echo 'You may need to run:'; \ +- echo ' make reconfigure'; \ +- exit 1; \ +- fi \ +- fi + + .PHONY: reconfigure + reconfigure: configure.sh +@@ -232,17 +223,6 @@ restatus: + Makefile: Makefile.in config.status configure.sh config.h.in + @if test -f Makefile; then cp -p Makefile Makefile.old; else touch Makefile.old; fi + @./config.status +- @if diff Makefile Makefile.old >/dev/null 2>&1; then \ +- echo "Makefile is unchanged."; \ +- rm Makefile.old; \ +- else \ +- if test "$(MAKECMDGOALS)" = reconfigure; then \ +- echo 'Continuing with "make reconfigure".'; \ +- else \ +- echo "Makefile updated -- rerun your make command."; \ +- exit 1; \ +- fi \ +- fi + + stunnel-rsyncd.conf: $(srcdir)/stunnel-rsyncd.conf.in Makefile + sed 's;\@bindir\@;$(bindir);g' <$(srcdir)/stunnel-rsyncd.conf.in >stunnel-rsyncd.conf diff --git a/debian/patches/series b/debian/patches/series new file mode 100644 index 000000000..0a5931483 --- /dev/null +++ b/debian/patches/series @@ -0,0 +1 @@ +disable_reconfigure_req.diff diff --git a/debian/rules b/debian/rules new file mode 100644 index 000000000..cbe925d75 --- /dev/null +++ b/debian/rules @@ -0,0 +1,3 @@ +#!/usr/bin/make -f +%: + dh $@ diff --git a/debian/watch b/debian/watch new file mode 100644 index 000000000..6c67a454c --- /dev/null +++ b/debian/watch @@ -0,0 +1,3 @@ +version=4 +opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/rsync-$1\.tar\.gz/ \ + https://github.com/twojstaryzdomu/rsync/releases .*/v?(\d\S+)\.tar\.gz diff --git a/flist.c b/flist.c index 17832533e..d163798f6 100644 --- a/flist.c +++ b/flist.c @@ -62,6 +62,7 @@ extern int implied_dirs; extern int ignore_perishable; extern int non_perishable_cnt; extern int prune_empty_dirs; +extern int update_links; extern int copy_links; extern int copy_unsafe_links; extern int protocol_version; @@ -234,10 +235,15 @@ static int readlink_stat(const char *path, STRUCT_STAT *stp, char *linkbuf) int link_stat(const char *path, STRUCT_STAT *stp, int follow_dirlinks) { #ifdef SUPPORT_LINKS - if (copy_links) + if (copy_links && update_links == 0) return x_stat(path, stp, NULL); if (x_lstat(path, stp, NULL) < 0) return -1; + if (update_links && S_ISLNK(stp->st_mode)) { + STRUCT_STAT st; + if (x_stat(path, &st, NULL) == 0 && !S_ISDIR(st.st_mode)) + return 0; + } if (follow_dirlinks && S_ISLNK(stp->st_mode)) { STRUCT_STAT st; if (x_stat(path, &st, NULL) == 0 && S_ISDIR(st.st_mode)) diff --git a/generator.c b/generator.c index b56fa569a..beec8df53 100644 --- a/generator.c +++ b/generator.c @@ -57,6 +57,9 @@ extern int ignore_errors; extern int remove_source_files; extern int delay_updates; extern int update_only; +extern int update_links; +extern int copy_links; +extern int allow_link_update_dir; extern int human_readable; extern int ignore_existing; extern int ignore_non_existing; @@ -1193,6 +1196,19 @@ static BOOL is_below(struct file_struct *file, struct file_struct *subtree) && (!implied_dirs_are_missing || f_name_has_prefix(file, subtree)); } +static BOOL dir_empty(char *dirname) { + int n = 3; + DIR *dir = opendir(dirname); + if (dir == NULL) + return -1; + while (readdir(dir)) { + if (!--n) + break; + } + closedir(dir); + return n; +} + /* Acts on the indicated item in cur_flist whose name is fname. If a dir, * make sure it exists, and has the right permissions/timestamp info. For * all other non-regular files (symlinks, etc.) we create them here. For @@ -1540,6 +1556,8 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, && hard_link_check(file, ndx, fname, statret, &sx, itemizing, code)) goto cleanup; #endif + if (DEBUG_GTE(GENR, 1)) + rprintf(FINFO, "%s src mtime=%ld dest mtime=%ld modify_window=%d statret=%d copy_links=%d update_links=%d allow_link_update_dir=%d\n", fname, file->modtime, (long)sx.st.st_mtime, modify_window, statret, copy_links, update_links, allow_link_update_dir); if (preserve_links && ftype == FT_SYMLINK) { #ifdef SUPPORT_LINKS @@ -1588,6 +1606,66 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, fnamecmp = fnamecmpbuf; } } + if (statret == 0) { + if (update_links > 0) { + if (S_ISDIR(sx.st.st_mode) && allow_link_update_dir == 0) { + if (INFO_GTE(SKIP, 1)) + rprintf(FINFO, "symlink \"%s\" is a directory on destination and allow-link-update-dir isn't enabled, skipping\n", fname); + goto cleanup; + } + else { + int mtime_offset = sx.st.st_mtime - file->modtime; + char *st_mode = S_ISDIR(sx.st.st_mode) + ? "directory" + : S_ISLNK(sx.st.st_mode) + ? "symlink" + : S_ISCHR(sx.st.st_mode) + ? "character device" + : S_ISBLK(sx.st.st_mode) + ? "block device" + : S_ISFIFO(sx.st.st_mode) + ? "named pipe" + : S_ISSOCK(sx.st.st_mode) + ? "socket" + : "file"; + if (mtime_offset > modify_window) { + if (INFO_GTE(SKIP, 1)) + rprintf(FINFO, "%s \"%s\" is newer by %d sec, skipping\n", st_mode, fname, mtime_offset - modify_window); + goto cleanup; + } + else if (mtime_offset < modify_window) { + if (S_ISDIR(sx.st.st_mode)) { + if (!dir_empty(fname)) { + rprintf(FINFO, "directory %s not empty, skipping\n", fname); + goto cleanup; + } + } + if (INFO_GTE(SKIP, 1)) + rprintf(FINFO, "%s \"%s\" is older by %d sec, updating\n", st_mode, fname, - mtime_offset - modify_window); + } + else if (S_ISLNK(sx.st.st_mode)) { + char lnk[MAXPATHLEN]; + int llen = do_readlink(fname, lnk, MAXPATHLEN - 1); + lnk[llen] = '\0'; + if (strcmp(lnk, F_SYMLINK(file)) == 0) { + if (INFO_GTE(SKIP, 1)) + rprintf(FINFO, "symlink \"%s\" points to the same referent %s, skipping\n", fname, lnk); + goto cleanup; + } + else if (INFO_GTE(SKIP, 1)) + rprintf(FINFO, "symlink \"%s\" points to a different referent on source (%s) than destination (%s), updating\n", fname, F_SYMLINK(file), lnk); + } + else { + if (INFO_GTE(SKIP, 1)) + rprintf(FINFO, "symlink \"%s\" more recent than %s on destination, updating\n", fname, st_mode); + } + } + } + else { + if (DEBUG_GTE(GENR, 1)) + rprintf(FINFO, "update-links not enabled for \"%s\"\n, skipping", fname); + } + } if (atomic_create(file, fname, sl, NULL, MAKEDEV(0, 0), &sx, statret == 0 ? DEL_FOR_SYMLINK : 0)) { set_file_attrs(fname, file, NULL, NULL, 0); if (itemizing) { diff --git a/options.c b/options.c index 578507c6e..521be4adc 100644 --- a/options.c +++ b/options.c @@ -48,6 +48,8 @@ int whole_file = -1; int append_mode = 0; int keep_dirlinks = 0; int copy_dirlinks = 0; +int update_links = 0; +int allow_link_update_dir = 0; int copy_links = 0; int copy_devices = 0; int write_devices = 0; @@ -693,6 +695,8 @@ static struct poptOption long_options[] = { {"no-one-file-system",0, POPT_ARG_VAL, &one_file_system, 0, 0, 0 }, {"no-x", 0, POPT_ARG_VAL, &one_file_system, 0, 0, 0 }, {"update", 'u', POPT_ARG_NONE, &update_only, 0, 0, 0 }, + {"update-links", 0, POPT_ARG_NONE, &update_links, 0, 0, 0 }, + {"allow-link-update-dir",0,POPT_ARG_NONE, &allow_link_update_dir, 0, 0, 0 }, {"existing", 0, POPT_ARG_NONE, &ignore_non_existing, 0, 0, 0 }, {"ignore-non-existing",0,POPT_ARG_NONE, &ignore_non_existing, 0, 0, 0 }, {"ignore-existing", 0, POPT_ARG_NONE, &ignore_existing, 0, 0, 0 }, @@ -1380,6 +1384,8 @@ int parse_arguments(int *argc_p, const char ***argv_p) * only special cases are returned and listed here. */ switch (opt) { + case 'L': + break; case 'V': version_opt_cnt++; break; @@ -2321,6 +2327,9 @@ int parse_arguments(int *argc_p, const char ***argv_p) parse_filter_str(&filter_list, backup_dir_buf, rule_template(0), 0); } + if (update_links) + preserve_links = 1; + if (make_backups && !backup_dir) omit_dir_times = -1; /* Implied, so avoid -O to sender. */ @@ -2615,7 +2624,7 @@ void server_options(char **args, int *argc_p) argstr[x++] = 'u'; if (!do_xfers) /* Note: NOT "dry_run"! */ argstr[x++] = 'n'; - if (preserve_links) + if (preserve_links || update_links) argstr[x++] = 'l'; if ((xfer_dirs >= 2 && xfer_dirs < 4) || (xfer_dirs && !recurse && (list_only || (delete_mode && am_sender)))) @@ -2634,6 +2643,11 @@ void server_options(char **args, int *argc_p) if (fuzzy_basis > 1) argstr[x++] = 'y'; } + if (update_links) { + args[ac++] = "--update-links"; + if (allow_link_update_dir) + args[ac++] = "--allow-link-update-dir"; + } } else { if (copy_links) argstr[x++] = 'L'; diff --git a/rsync.1.md b/rsync.1.md index 7e40e3617..a9fe9dcac 100644 --- a/rsync.1.md +++ b/rsync.1.md @@ -431,6 +431,8 @@ has its own detailed description later in this manpage. --backup-dir=DIR make backups into hierarchy based in DIR --suffix=SUFFIX backup suffix (default ~ w/o --backup-dir) --update, -u skip files that are newer on the receiver +--update-links skip symlinks that are newer on the receiver +--allow-link-update-dir newer symlinks may replace older directories --inplace update destination files in-place --append append data onto shorter files --append-verify --append w/old data in file checksum @@ -1034,11 +1036,12 @@ expand it. will be updated if the sizes are different.) Note that this does not affect the copying of dirs, symlinks, or other - special files. Also, a difference of file format between the sender and - receiver is always considered to be important enough for an update, no - matter what date is on the objects. In other words, if the source has a - directory where the destination has a file, the transfer would occur - regardless of the timestamps. + special files unless the `--update-links` option is enabled. Also, + a difference of file format between the sender and receiver is always + considered to be important enough for an update, no matter what date is + on the objects. In other words, if the source has a directory where + the destination has a file, the transfer would occur regardless of + the timestamps. This option is a [TRANSFER RULE](#TRANSFER_RULES), so don't expect any exclude side effects. @@ -1050,6 +1053,49 @@ expand it. is usually best to avoid combining this with[ `--inplace`](#opt) unless you have implemented manual steps to handle any interrupted in-progress files. +0. `--update-links` + + This option controls rsync behaviour when a symbolic link on the source + encounters a destination file in the way of the symlink during transfer. + The setting forces rsync to skip any files which exist on the destination + and have a modified time that is newer than the symbolic link on the + source. A file in the way of the symlink can be a regular file, a symlink, + a named pipe, or a device that exists on the destination. Such a file with + an identical or less recent modification time than the source symlink will + be deleted and a symlink pointing to the same item (the referent) as the + source symlink on the destination created in its place. A destination + symlink with an identical modification time as the source symlink will + only be replaced if the referent is different. By default, any directory + in the way of the source symlink is exempted from removal by a symlink, + regardless of its modification time. To extend the behaviour of this + option to directories, see the [`--allow-link-update-dir`](#opt) option + below. + + This option may be specified independently of the [`--update`](#opt) + ([`-u`](#opt)) option mentioned above, which acts on regular source files + only. + + If specified in tandem, `--update-links` prevails over + [`--copy-links`](#opt) and [`--links`](#opt). + +0. `--allow-link-update-dir` + + Enabling this option allows rsync to replace a less recent empty directory + on the destination with a more recent symbolic link from the source. This + option extends the [`--update-links`](#opt) option, which by default + preserves any existing directory on the destination from being replaced + by a more recent symbolic link. The modification time of the symbolic link + on the source needs to be more recent than the modification time of the + directory existing on the destination for this option to take effect. An + exisitng directory with a more recent modification time on the destination + than the symlink on the source will not be removed and replaced by + the symlink. A destination directory with an identical modification time + as the source symbolic link will be removed and a symlink will be recreated + with same referent as the source symlink. Non-empty directories are not + unlinked under any circumstances. + + This option requires the [`--update-links`](#opt) option to be enabled. + 0. `--inplace` This option changes how rsync transfers a file when its data needs to be @@ -1179,7 +1225,12 @@ expand it. alternately silence the warning by specifying [`--info=nonreg0`](#opt). The default handling of symlinks is to recreate each symlink's unchanged - value on the receiving side. + value on the receiving side. To exempt more recent destination + files from being replaced by a link, refer to the [`--update-links`](#opt) + option. + + If specified in tandem, [`--update-links`](#opt) or [`--copy-links`](#opt) + prevail over `--links`. See the [SYMBOLIC LINKS](#) section for multi-option info. @@ -1190,8 +1241,8 @@ expand it. references. If a symlink chain is broken, an error is output and the file is dropped from the transfer. - This option supersedes any other options that affect symlinks in the - transfer, since there are no symlinks left in the transfer. + This option supersedes any other option that affect symlinks in the + transfer but [`--update-links`](#opt), which takes precedence. This option does not change the handling of existing symlinks on the receiving side, unlike versions of rsync prior to 2.6.3 which had the @@ -4568,7 +4619,7 @@ version uses a new implementation. ## SYMBOLIC LINKS -Three basic behaviors are possible when rsync encounters a symbolic +Four basic behaviors are possible when rsync encounters a symbolic link in the source directory. By default, symbolic links are not transferred at all. A message "skipping @@ -4582,6 +4633,15 @@ implies [`--links`](#opt). If [`--copy-links`](#opt) is specified, then symlinks are "collapsed" by copying their referent, rather than the symlink. +If [`--update-links`](#opt) is specified, then symlinks are recreated with +the same target on the destination unless the existing destination file +in the way of the symlink is newer or is a directory. +Unlike [`--links`](#opt), this option preserves all more recent files on the +destination from being replaced by a symlink. It is possible to extend having +an existing less recent directory on the destination replaced by a more recent +symlink by specifying [`--allow-link-update-dir`](#opt) in addition to +[`--update-links`](#opt). + Rsync can also distinguish "safe" and "unsafe" symbolic links. An example where this might be used is a web site mirror that wishes to ensure that the rsync module that is copied does not include symbolic links to `/etc/passwd` in @@ -4610,6 +4670,22 @@ first line that is a complete subset of your options: 0. `--links --safe-links` The receiver skips creating unsafe symlinks found in the transfer and creates the safe ones. 0. `--links` Create all symlinks. +0. `--update-links` Create symlinks, replace less recent files or files + with an identical modification time. Skip any more recent destination + files. Unconditionally exclude any existing directories on the destination + from deletion and replacement by a symlink when one would otherwise + be transferred. +0. `--update-links --allow-link-update-dir` Create symlinks, skip any more + recent files existing on the destination, allow a less recent + directory to be replaced by a more recent symlink. + +Note that some options are mutually exclusive. The first option listed +below prevails over the others, when multiple symlink mode options are +specified. + +0. `--update-links` +0. `--copy-links` +0. `--links` For the effect of [`--munge-links`](#opt), see the discussion in that option's section. diff --git a/testsuite/update-links.test b/testsuite/update-links.test new file mode 100644 index 000000000..c81565f60 --- /dev/null +++ b/testsuite/update-links.test @@ -0,0 +1,610 @@ +#! /bin/sh + +SELF="$(basename "$0")" +LSRC="$(readlink -f "$(realpath $0)")" +LSELF="$(basename "$LSRC")" +DSELF="$(dirname "$LSRC")" + +. "${testsuite:-${DSELF}}/rsync.fns" + +set_os_opts(){ + os=$(uname -s) + case ${os} in + Linux) + stat_opt=-c + size=%s + # ctime is not normally modifiable by user, defaults to mtime + ctime=%Y + mtime=%Y + ;; + FreeBSD) + stat_opt=-f + size=%z + # ctime is not normally modifiable by user, defaults to mtime + ctime=%Sm + mtime=%Sm + bsd="-t %s" + ;; + *) + test_fail "OS ${os} unsupported" + ;; + esac +} + +log(){ + echo "${SELF}: $@" +} + +debug(){ + [ -n "$DEBUG" ] && echo "$@" 1>&2 || : +} + +error(){ + test_fail "$@" +} + +get_ctime(){ + stat ${stat_opt} ${ctime} ${bsd} "$1" || test_fail "Unable to get ctime of $1" +} + +get_mtime(){ + stat ${stat_opt} ${mtime} ${bsd} "$1" || test_fail "Unable to get mtime of $1" +} + +get_size(){ + stat ${stat_opt} ${size} "$1" || test_fail "Unable to get size of $1" +} + +get_referent(){ + readlink "$1" || test_fail "Unable to dereference $1" +} + +get_type(){ + file -b "$1" || test_fail "Unable to get type of $1" +} + +exists(){ + [ -h "$1" ] || [ -e "$1" ] +} + +is_a_link(){ + [ -h "$1" ] +} + +is_a_file(){ + [ -h "$1" ] +} + +is_a_pipe(){ + [ -p "$1" ] +} + +points_to_pipe(){ + is_a_link "$1" && is_a_pipe "$1" +} + +is_a_dir(){ + ! is_a_link "$1" && [ -d "$1" ] +} + +is_empty_dir(){ + is_a_dir "$1" && [ $(find "$1" | wc -l) -eq 1 ] +} + +points_to_dir(){ + is_a_link "$1" && [ -d "$1" ] +} + +is_newer(){ + [ $(get_ctime "$1") -gt $(get_ctime "$2") ] || error "$1 is not newer than $2" + #DO NOT USE, DEREFERENCES LINKS! [ "$1" -nt "$2" ] +} + +is_older(){ + [ $(get_ctime "$1") -lt $(get_ctime "$2") ] || error "$1 is not older than $2" + #DO NOT USE, DEREFERENCES LINKS! [ "$1" -ot "$2" ] +} + +is_defined(){ + type "$1" >/dev/null 2>&1 +} + +compare(){ + local s + local d + s=$($1 "$2") + debug "compare $1 $2: output = $s; exit status = $?" + d=$($1 "$3") + debug "compare $1 $3: output = $d; exit status = $?" + case "$s" in + "$d") + : + ;; + *) + return 1 + ;; + esac +} + +# The size of a symlink equals the length of the link target path +# Links pointing at /dev/zero & /dev/null will thus have the same size +is_same_size(){ + compare get_size "$1" "$2" || test_fail "Files $1 & $2 are not same size" +} + +is_diff_size(){ + compare get_size "$1" "$2" && test_fail "Files $1 & $2 are supposed to be of different size" || : +} + +is_same_mtime(){ + compare get_mtime "$1" "$2" || test_fail "Files $1 & $2 have different mtimes" +} + +is_diff_mtime(){ + compare get_mtime "$1" "$2" && test_fail "Files $1 & $2 are supposed to have different mtimes" || : +} + +is_any_mtime(){ + compare get_mtime "$1" "$2" || : +} + +is_same_ctime(){ + compare get_ctime "$1" "$2" || test_fail "Files $1 & $2 have different ctimes" +} + +is_diff_ctime(){ + compare get_ctime "$1" "$2" && test_fail "Files $1 & $2 are supposed to have different ctimes" || : +} + +is_any_ctime(){ + compare get_ctime "$1" "$2" || : +} + +is_same_referent(){ + compare get_referent "$1" "$2" || test_fail "Links $1 & $2 point at a different referent each" +} + +is_diff_referent(){ + compare get_referent "$1" "$2" && test_fail "Links $1 & $2 should point at a different referent each" || : +} + +touch_links(){ + debug "touch_links: # touch -h $@" + touch -h "$@" +} + +shift_mtime(){ + local offset + local f + local epoch + local rc + local time + offset=$1 + shift + for f in $@; do + epoch=$(stat ${stat_opt} ${mtime} ${bsd} "$f") + rc=$? + debug "shift_mtime: $f; offset = $offset; rc = $rc; touch -hd @$((epoch${offset})) $f" + case ${os} in FreeBSD) time=$(date -r $((epoch${offset})) +%Y-%m-%dT%H:%M:%S) ;; *) time=@$((epoch${offset})) ;; esac + [ $rc -eq 0 ] && touch -hd "${time}" "$f" || error "Unable to stat $f" + done +} + +add_a_second(){ + shift_mtime +1 $@ +} + +take_a_second(){ + shift_mtime -1 $@ +} + +show_files(){ + stat ${stat_opt} "${mtime} ${ctime} %N" ${bsd} $@ 1>&2 || test_fail "Unable to stat $@" +} + +count_words(){ + echo $@ | wc -w +} + +get_word(){ + local i + local words + i=$1 + shift + words=$@ + while [ $i -gt 0 ]; do + words=${words#* } + i=$(($i-1)) + done + echo ${words%% *} +} + +# Sets the referent of $1 to something else than the existing one and the referent of $2 +# choice of out of two variables, purposely defined to be of different length +# size checks should pass +cycle_referent(){ + local links + local referent + local current + local n + local next + links="/dev/urandom /dev/random" + referent=$(get_referent "$1") + [ -n "$2" ] && compare_referent=$(get_referent "$2") + n=0; while [ $n -le $(count_words $links) ]; do + current=$(get_word $n $links) + n=$((n+1)) + [ -n "$2" ] || next="$(get_word $n $links)" + case "$current" in + ${compare_referent}|${referent}) + : + ;; + *) + ln -nsf "$current" "$1" && return + ;; + esac + done || test_fail "Unable to cycle referent for $1" +} + +build_symlinks(){ + rm -rf "$fromdir" + mkdir "$fromdir" + mkdir "$fromdir/emptydir" + mkdir "$fromdir/nonemptydir" + touch "$fromdir/nonemptydir/file" + mkfifo "$fromdir/fifo" + mkfifo "$fromdir/fifo" + date >"$fromdir/referent" + ln -s referent "$fromdir/relative" + ln -s "$fromdir/referent" "$fromdir/absolute" + ln -s nonexistent "$fromdir/dangling" + ln -s "$srcdir/rsync.c" "$fromdir/unsafe" + ln -s emptydir "$fromdir/dirlink" + ln -s /dev/null "$fromdir/devlink" +} + +# Unused yet +is_set(){ + read opt opts << _EOT_ +$@ +_EOT_ + case "${opts}" in + "${opt}"|"${opt} "*|*" ${opt}"*) + : + ;; + *) + false + ;; + esac +} + +list_contains(){ + read opt opts << _EOT_ +$@ +_EOT_ + debug "list_contains: opt = $opt; opts = $opts" + echo "$opts" | egrep -q -- " ${opt} |^${opt} | ${opt}$" 1>/dev/null +} + +# Various scenarios that modify either side before re-running rsync & performing a check +default(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + show_files $fromdir/$file + exists $todir/$file && show_files $todir/$file || : +} + +newer_dest_link(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + if exists $todir/$file; then + if is_a_link $todir/$file; then + take_a_second "$fromdir/$file" + add_a_second "$todir/$file" + show_files "$todir/$file" "$fromdir/$file" + fi + else + show_files "$fromdir/$file" + log "$todir/$file did not get transferred in scenario $scenario with rsync opts $opts" + fi +} + +newer_source_link(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + is_a_link $fromdir/$file && touch_links $fromdir/$file $todir/$file && add_a_second $fromdir/$file || : +} + +cycle_dest_link(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + if is_a_link $todir/$file; then + cycle_referent $todir/$file $fromdir/$file + # Simulate a time difference, making dest_link more recent + take_a_second $fromdir/$file + add_a_second $todir/$file + #show_files $fromdir/$file $todir/$file + fi +} + +cycle_dest_newer_source_link(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + cycle_dest_link $fromdir $todir $file $scenario $opts && newer_source_link $fromdir $todir $file $scenario $opts + if exists $todir/$file; then + show_files "$todir/$file" "$fromdir/$file" + else + show_files "$fromdir/$file" + log "$todir/$file did not get transferred in scenario $scenario with rsync opts $opts" + fi +} + +link_update_dir(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + # Make all target files directories + local f + for f in $FILES; do + debug "link_update_dir: $todir/$f" + rm $todir/$f 2>/dev/null || log "${scenario}: $f didn't exist" + mkdir -p $todir/$f + case $f in + nonemptydir) + touch $todir/$f/testfile + ;; + esac + # Make source link more recent than dir just created + add_a_second $fromdir/$f + show_files $todir/$f + done +} + +test_failure(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + debug "${scenario}: has predictably failed" + false +} + +test_success(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + debug "${scenario}: has predictably completed" + true +} + +# Various checks after a scenario has run, these rules should all pass +run_checks(){ + read type fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + local times + list_contains "--times" "$opts" || times=any + for check in ${times:-${type}}_mtime ${times:-${type}}_ctime ${type}_referent ${type}_size; do + debug "check_${scenario}: is_${check} $fromdir/$file $todir/$file" + is_${check} $fromdir/$file $todir/$file + done +} + +check_existing_dest(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + run_checks same $fromdir $todir $file $scenario $opts +} + +check_default(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + exists $todir/$file && check_existing_dest $fromdir $todir $file $scenario $opts || debug "$todir/$file does not exist, no more checks" +} + +check_newer_dest_link(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + if is_a_link $todir/$file; then + if list_contains "--update-links" "$opts"; then + # Newer dest links are supposed to be preserved + is_same_referent $fromdir/$file $todir/$file + # Times will get updated if times is in effect + if list_contains "--times" "$opts"; then + is_same_mtime $fromdir/$file $todir/$file + is_same_ctime $fromdir/$file $todir/$file + else + is_older $fromdir/$file $todir/$file + fi + fi + fi +} + +check_newer_source_link(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + if is_a_link $todir/$file; then + if list_contains "--update-links" "$opts"; then + # Newer source links are supposed to be transfered + run_checks same $fromdir $todir $file $scenario $opts + fi + fi +} + +check_cycle_dest_link(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + if is_a_link $todir/$file; then + if list_contains "--update-links" "$opts"; then + # Newer source links are supposed to be transfered + run_checks diff $fromdir $todir $file $scenario $opts + fi + fi +} + +check_cycle_dest_newer_source_link(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + check_newer_source_link $fromdir $todir $file $scenario $opts +} + +check_link_update_dir(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + if is_a_link $todir/$file; then + if list_contains "--update-links" "$opts"; then + if list_contains "--allow-link-update-dir" "$opts"; then + case $file in + nonemptydir) + error "$file should not have been replaced by a link" + ;; + esac + else + case $file in + *dir) + error "$file should not have been replaced by a link without --allow-link-update-dir enabled" + ;; + esac + fi + else + error "$file should not have been replaced by a link without --update-links enabled" + fi + # Newer source links are supposed to be transfered + run_checks same $fromdir $todir $file $scenario $opts + fi +} + +check_test_failure(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + debug "check_${scenario}: $scenario should have failed, this notice should never be displayed" + exit 1 +} + +check_test_success(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + debug "check_${scenario}: $scenario should have completed, this notice should always be displayed" +} + +run_rsync(){ + read fromdir todir file opts << _EOT_ +$@ +_EOT_ + debug "run_rsync: # $RSYNC $RSYNC_COMMON_OPTS $opts $fromdir/$file $todir/" + $RSYNC $RSYNC_COMMON_OPTS $opts $fromdir/$file $todir/ 1>&2 || test_fail "$RSYNC $RSYNC_COMMON_OPTS $opts $fromdir/$file $todir/ failed" +} + +run_scenario(){ + read fromdir todir file scenario opts << _EOT_ +$@ +_EOT_ + cd $tmpdir && build_symlinks || error "Unable to change directory to $tmpdir" + debug "run_scenario: scenario = $scenario; file = $file; type = $(get_type $fromdir/$file); opts = $opts" + run_rsync $fromdir $todir $file $opts + is_defined $scenario && $scenario $fromdir $todir $file $scenario $opts + run_rsync $fromdir $todir $file $opts + check_${scenario} $fromdir $todir $file $scenario $opts + rm -rf $fromdir $todir +} + +run_scenarios(){ + read fromdir todir file opts << _EOT_ +$@ +_EOT_ + local scenario + for scenario in $SCENARIOS; do + for t in '' check_; do + is_defined ${t}${scenario} || error "No ${t}${scenario} scenario defined" + done + log "Scenario $scenario running on $file with rsync opts $opts" + run_scenario $fromdir $todir $file $scenario $opts + done +} + +process_files(){ + read fromdir todir opts << _EOT_ +$@ +_EOT_ + local file + for file in $FILES; do + run_scenarios $fromdir $todir $file $opts + done +} + +sort_opts(){ + echo "$@" | tr ' ' '\n' | sort | xargs echo +} + +strip_whitespace(){ + echo "${@}" | sed -e 's/ //g' +} + +process_opts(){ + read fromdir todir opts << _EOT_ +$@ +_EOT_ + local current + local opt + debug "process_opts: opts = $opts" + for opt in $opts; do + debug "process_opts: opt = $opt; done = $done" + current="$(sort_opts $current $opt)" + if list_contains "$(strip_whitespace ${current})" "$done"; then + # Skip item if found on the done list + debug "process_opts: $current already processed" + continue + else + debug "process_opts: current = $current" + process_files $fromdir $todir $current + done="${done:+${done} }$(strip_whitespace ${current})" + fi + debug "process_opts: opt = $opt; opts = $opts; current = $current" + done +} + +process_all_opts(){ + read fromdir todir all_opts << _EOT_ +$@ +_EOT_ + local remaining + local first + remaining=${all_opts} + debug "process_all_opts: remaining = ${remaining}" + first=${remaining%% *} + while [ -n "$remaining" ]; do + debug "process_all_opts: remaining before run = ${remaining}" + process_opts $fromdir $todir ${remaining} + # Remove first parameter for next iteration + remaining="${remaining##${remaining%% *} } ${remaining%% *}" + debug "process_all_opts: remaining after run = ${remaining}" + case ${remaining%% *} in ${first}) break;; esac + done + log "Completed scenarios with status $?" + #echo -e $n | wc -c +} + +# All of the following variables may be overriden at command line +FILES="${FILES:-relative dangling absolute unsafe devlink dirlink emptydir nonemptydir}" +# For sanity testing only +#SCENARIOS="test_success test_failure" +SCENARIOS="${SCENARIOS:-default newer_source_link newer_dest_link cycle_dest_link cycle_dest_newer_source_link link_update_dir}" +RSYNC=${RSYNC:-${DSELF}/../rsync} +[ -n "$DEBUG" ] && RSYNC_COMMON_OPTS=${RSYNC_COMMON_OPTS:--r -i} +RSYNC_OPTS="--update-links --times --allow-link-update-dir" + +set_os_opts +process_all_opts $fromdir $todir ${RSYNC_OPTS}