From 01742c07e6d7559d69e101a7c2e5380179c08555 Mon Sep 17 00:00:00 2001 From: Wayne Davison Date: Thu, 23 Jul 2020 20:46:51 -0700 Subject: [PATCH] Add --mkpath option. Fixes bugzilla bug 4621. --- NEWS.md | 3 +++ main.c | 34 ++++++++++++++++++++++++++-------- options.c | 6 ++++++ rsync.1.md | 21 +++++++++++++++++++++ testsuite/mkpath.test | 43 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 8 deletions(-) create mode 100755 testsuite/mkpath.test diff --git a/NEWS.md b/NEWS.md index a0ec31e0..27e8eef8 100644 --- a/NEWS.md +++ b/NEWS.md @@ -42,6 +42,9 @@ - Added `--crtimes` (`-N`) option for preserving the file's create time (on an OS that supports that, such as macOS). + - Added `--mkpath` option to tell rsync that it should create a non-existing + path component of the destination arg. + - Added the ability to specify "@netgroup" names to the `hosts allow` and `hosts deny` daemon parameters. This is a finalized version of the netgroup-auth patch from the patches repo. diff --git a/main.c b/main.c index fcc0e65e..1130e24c 100644 --- a/main.c +++ b/main.c @@ -57,6 +57,7 @@ extern int copy_unsafe_links; extern int keep_dirlinks; extern int preserve_hard_links; extern int protocol_version; +extern int mkpath_dest_arg; extern int file_total; extern int recurse; extern int xfer_dirs; @@ -677,7 +678,7 @@ static pid_t do_cmd(char *cmd, char *machine, char *user, char **remote_argv, in static char *get_local_name(struct file_list *flist, char *dest_path) { STRUCT_STAT st; - int statret; + int statret, trailing_slash; char *cp; if (DEBUG_GTE(RECV, 1)) { @@ -710,7 +711,26 @@ static char *get_local_name(struct file_list *flist, char *dest_path) } /* See what currently exists at the destination. */ - if ((statret = do_stat(dest_path, &st)) == 0) { + statret = do_stat(dest_path, &st); + cp = strrchr(dest_path, '/'); + trailing_slash = cp && !cp[1]; + + if (mkpath_dest_arg && statret < 0 && (cp || file_total > 1)) { + int ret = make_path(dest_path, file_total > 1 && !trailing_slash ? 0 : MKP_DROP_NAME); + if (ret < 0) + goto mkdir_error; + if (INFO_GTE(NAME, 1)) { + if (file_total == 1 || trailing_slash) + *cp = '\0'; + rprintf(FINFO, "created %s %s\n", ret == 1 ? "directory" : "path", dest_path); + if (file_total == 1 || trailing_slash) + *cp = '/'; + } + if (file_total > 1 || trailing_slash) + statret = do_stat(dest_path, &st); + } + + if (statret == 0) { /* If the destination is a dir, enter it and use mode 1. */ if (S_ISDIR(st.st_mode)) { if (!change_dir(dest_path, CD_NORMAL)) { @@ -740,15 +760,12 @@ static char *get_local_name(struct file_list *flist, char *dest_path) exit_cleanup(RERR_FILESELECT); } - cp = strrchr(dest_path, '/'); - /* If we need a destination directory because the transfer is not * of a single non-directory or the user has requested one via a * destination path ending in a slash, create one and use mode 1. */ - if (file_total > 1 || (cp && !cp[1])) { - /* Lop off the final slash (if any). */ - if (cp && !cp[1]) - *cp = '\0'; + if (file_total > 1 || trailing_slash) { + if (trailing_slash) + *cp = '\0'; /* Lop off the final slash (if any). */ if (statret == 0) { rprintf(FERROR, "ERROR: destination path is not a directory\n"); @@ -756,6 +773,7 @@ static char *get_local_name(struct file_list *flist, char *dest_path) } if (do_mkdir(dest_path, ACCESSPERMS) != 0) { + mkdir_error: rsyserr(FERROR, errno, "mkdir %s failed", full_fname(dest_path)); exit_cleanup(RERR_FILEIO); diff --git a/options.c b/options.c index 2ef2dbf2..d92a7665 100644 --- a/options.c +++ b/options.c @@ -104,6 +104,7 @@ int eol_nulls = 0; int protect_args = -1; int human_readable = 1; int recurse = 0; +int mkpath_dest_arg = 0; int allow_inc_recurse = 1; int xfer_dirs = -1; int am_daemon = 0; @@ -1017,6 +1018,8 @@ static struct poptOption long_options[] = { {"8-bit-output", '8', POPT_ARG_VAL, &allow_8bit_chars, 1, 0, 0 }, {"no-8-bit-output", 0, POPT_ARG_VAL, &allow_8bit_chars, 0, 0, 0 }, {"no-8", 0, POPT_ARG_VAL, &allow_8bit_chars, 0, 0, 0 }, + {"mkpath", 0, POPT_ARG_VAL, &mkpath_dest_arg, 1, 0, 0 }, + {"no-mkpath", 0, POPT_ARG_VAL, &mkpath_dest_arg, 0, 0, 0 }, {"qsort", 0, POPT_ARG_NONE, &use_qsort, 0, 0, 0 }, {"copy-as", 0, POPT_ARG_STRING, ©_as, 0, 0, 0 }, {"address", 0, POPT_ARG_STRING, &bind_address, 0, 0, 0 }, @@ -3115,6 +3118,9 @@ void server_options(char **args, int *argc_p) if (open_noatime && preserve_atimes <= 1) args[ac++] = "--open-noatime"; + if (mkpath_dest_arg && am_sender) + args[ac++] = "--mkpath"; + if (ac > MAX_SERVER_ARGS) { /* Not possible... */ rprintf(FERROR, "argc overflow in server_options().\n"); exit_cleanup(RERR_MALLOC); diff --git a/rsync.1.md b/rsync.1.md index 4a2b5aeb..1aa8f5c4 100644 --- a/rsync.1.md +++ b/rsync.1.md @@ -351,6 +351,7 @@ detailed description below for a complete description. --append append data onto shorter files --append-verify --append w/old data in file checksum --dirs, -d transfer directories without recursing +--mkpath create the destination's path component --links, -l copy symlinks as symlinks --copy-links, -L transform symlink into referent file/dir --copy-unsafe-links only "unsafe" symlinks are transformed @@ -977,6 +978,26 @@ your home directory (remove the '=' for that). `--old-d`) that tells rsync to use a hack of `-r --exclude='/*/*'` to get an older rsync to list a single directory without recursing. +0. `--mkpath` + + Create a missing path component of the destination arg. This allows rsync + to create multiple levels of missing destination dirs and to create a path + in which to put a single renamed file. Keep in mind that you'll need to + supply a trailing slash if you want the entire destination path to be + treated as a directory when copying a single arg (making rsync behave the + same way that it would if the path component of the destination had already + existed). + + For example, the following creates a copy of file foo as bar in the sub/dir + directory, creating dirs "sub" and "sub/dir" if either do not yet exist: + + > rsync -ai --mkpath foo sub/dir/bar + + If you instead ran the following, it would have created file foo in the + sub/dir/bar directory: + + > rsync -ai --mkpath foo sub/dir/bar/ + 0. `--links`, `-l` When symlinks are encountered, recreate the symlink on the destination. diff --git a/testsuite/mkpath.test b/testsuite/mkpath.test new file mode 100755 index 00000000..6efb2105 --- /dev/null +++ b/testsuite/mkpath.test @@ -0,0 +1,43 @@ +#!/bin/sh + +. "$suitedir/rsync.fns" + +makepath "$fromdir" +makepath "$todir" + +cp_p "$srcdir/rsync.h" "$fromdir/text" +cp_p "$srcdir/configure.ac" "$fromdir/extra" + +cd "$tmpdir" + +deep_dir=to/foo/bar/baz/down/deep + +# Check that we can create several levels of dest dir +$RSYNC -aiv --mkpath from/text $deep_dir/new +test -f $deep_dir/new || test_fail "'new' file not found in $deep_dir dir" +rm -rf to/foo + +$RSYNC -aiv --mkpath from/text $deep_dir/ +test -f $deep_dir/text || test_fail "'text' file not found in $deep_dir dir" +rm $deep_dir/text + +# Make sure we can handle an existing path +mkdir $deep_dir/new +$RSYNC -aiv --mkpath from/text $deep_dir/new +test -f $deep_dir/new/text || test_fail "'text' file not found in $deep_dir/new dir" +rm -rf to/foo + +# Try the tests again with multiple source args +$RSYNC -aiv --mkpath from/ $deep_dir +test -f $deep_dir/extra || test_fail "'extra' file not found in $deep_dir dir" +rm -rf to/foo + +$RSYNC -aiv --mkpath from/ $deep_dir/ +test -f $deep_dir/text || test_fail "'text' file not found in $deep_dir dir" + +# Make sure that we can handle no path +$RSYNC -aiv --mkpath from/text to_text +test -f to_text || test_fail "'to_text' file not found in current dir" + +# The script would have aborted on error, so getting here means we've won. +exit 0 -- 2.34.1