rsync: add --one-file-system

classic Classic list List threaded Threaded
5 messages Options
Reply | Threaded
Open this post in threaded view
|

rsync: add --one-file-system

Björn Ketelaars
Add --one-file-system, which prevents openrsync to cross filesystem
boundaries. Option and behaviour is the same as GPL rsync.

OK?


diff --git usr.bin/rsync/extern.h usr.bin/rsync/extern.h
index 12aceae566c..602cd24cede 100644
--- usr.bin/rsync/extern.h
+++ usr.bin/rsync/extern.h
@@ -117,6 +117,7 @@ struct opts {
  int devices; /* --devices */
  int specials; /* --specials */
  int numeric_ids; /* --numeric-ids */
+ int one_file_system; /* --one-file-system or -x */
  char *rsync_path; /* --rsync-path */
  char *ssh_prog; /* --rsh or -e */
  char *port; /* --port */
diff --git usr.bin/rsync/flist.c usr.bin/rsync/flist.c
index b39599583e3..7f18966d85d 100644
--- usr.bin/rsync/flist.c
+++ usr.bin/rsync/flist.c
@@ -804,7 +804,7 @@ flist_gen_dirent(struct sess *sess, char *root, struct flist **fl, size_t *sz,
     size_t *max)
 {
  char *cargv[2], *cp;
- int rc = 0;
+ int rc = 0, xdev = 0;
  FTS *fts;
  FTSENT *ent;
  struct flist *f;
@@ -890,9 +890,15 @@ flist_gen_dirent(struct sess *sess, char *root, struct flist **fl, size_t *sz,
  * files and directory components, so use fts(3).
  * Copying the information file-by-file into the flstat.
  * We'll make sense of it in flist_send.
+ * If one-file-system is used, prevent fts(3) from descending
+ * into directories that have a different device number than
+ * the file from which the descent began.
  */
 
- if ((fts = fts_open(cargv, FTS_PHYSICAL, NULL)) == NULL) {
+ if (sess->opts->one_file_system)
+ xdev = FTS_XDEV;
+
+ if ((fts = fts_open(cargv, FTS_PHYSICAL | xdev, NULL)) == NULL) {
  ERR(sess, "fts_open");
  return 0;
  }
@@ -1129,7 +1135,7 @@ flist_gen_dels(struct sess *sess, const char *root, struct flist **fl,
     size_t *sz, const struct flist *wfl, size_t wflsz)
 {
  char **cargv = NULL;
- int  rc = 0, c;
+ int  rc = 0, c, xdev = 0;
  FTS *fts = NULL;
  FTSENT *ent;
  struct flist *f;
@@ -1235,9 +1241,15 @@ flist_gen_dels(struct sess *sess, const char *root, struct flist **fl,
  * Now we're going to try to descend into all of the top-level
  * directories stipulated by the file list.
  * If the directories don't exist, it's ok.
+ * If one-file-system is used, prevent fts(3) from descending
+ * into directories that have a different device number than
+ * the file from which the descent began.
  */
 
- if ((fts = fts_open(cargv, FTS_PHYSICAL, NULL)) == NULL) {
+ if (sess->opts->one_file_system)
+ xdev = FTS_XDEV;
+
+ if ((fts = fts_open(cargv, FTS_PHYSICAL | xdev, NULL)) == NULL) {
  ERR(sess, "fts_open");
  goto out;
  }
diff --git usr.bin/rsync/main.c usr.bin/rsync/main.c
index dd598d93eb2..3dbf6dd6028 100644
--- usr.bin/rsync/main.c
+++ usr.bin/rsync/main.c
@@ -305,6 +305,7 @@ main(int argc, char *argv[])
  { "no-times", no_argument, &opts.preserve_times, 0 },
  { "verbose", no_argument, &opts.verbose, 1 },
  { "no-verbose", no_argument, &opts.verbose, 0 },
+ { "one-file-system", no_argument, NULL, 'x' },
  { NULL, 0, NULL, 0 }};
 
  /* Global pledge. */
@@ -315,7 +316,7 @@ main(int argc, char *argv[])
 
  memset(&opts, 0, sizeof(struct opts));
 
- while ((c = getopt_long(argc, argv, "Dae:ghlnoprtvz", lopts, NULL))
+ while ((c = getopt_long(argc, argv, "Dae:ghlnoprtvxz", lopts, NULL))
     != -1) {
  switch (c) {
  case 'D':
@@ -359,6 +360,9 @@ main(int argc, char *argv[])
  case 'v':
  opts.verbose++;
  break;
+ case 'x':
+ opts.one_file_system = 1;
+ break;
  case 'z':
  fprintf(stderr, "%s: -z not supported yet\n", getprogname());
  break;
diff --git usr.bin/rsync/rsync.1 usr.bin/rsync/rsync.1
index c16168fff8f..911c848da7e 100644
--- usr.bin/rsync/rsync.1
+++ usr.bin/rsync/rsync.1
@@ -141,6 +141,8 @@ Increase verbosity.
 Specify once for files being transferred, twice for specific status,
 thrice for per-file transfer information, and four times for per-file
 breakdowns.
+.It Fl x , -one-file-system
+Do not cross filesystem boundaries.
 .It Fl -version
 Print version and exit.
 .El

Reply | Threaded
Open this post in threaded view
|

Re: rsync: add --one-file-system

Sebastian Benoit-3
You are missing usage.
I think its ok to just add -x to it, not the long option.

Otherwise ok benno@

Bj??rn Ketelaars([hidden email]) on 2019.04.01 21:36:34 +0200:

> Add --one-file-system, which prevents openrsync to cross filesystem
> boundaries. Option and behaviour is the same as GPL rsync.
>
> OK?
>
>
> diff --git usr.bin/rsync/extern.h usr.bin/rsync/extern.h
> index 12aceae566c..602cd24cede 100644
> --- usr.bin/rsync/extern.h
> +++ usr.bin/rsync/extern.h
> @@ -117,6 +117,7 @@ struct opts {
>   int devices; /* --devices */
>   int specials; /* --specials */
>   int numeric_ids; /* --numeric-ids */
> + int one_file_system; /* --one-file-system or -x */
>   char *rsync_path; /* --rsync-path */
>   char *ssh_prog; /* --rsh or -e */
>   char *port; /* --port */
> diff --git usr.bin/rsync/flist.c usr.bin/rsync/flist.c
> index b39599583e3..7f18966d85d 100644
> --- usr.bin/rsync/flist.c
> +++ usr.bin/rsync/flist.c
> @@ -804,7 +804,7 @@ flist_gen_dirent(struct sess *sess, char *root, struct flist **fl, size_t *sz,
>      size_t *max)
>  {
>   char *cargv[2], *cp;
> - int rc = 0;
> + int rc = 0, xdev = 0;
>   FTS *fts;
>   FTSENT *ent;
>   struct flist *f;
> @@ -890,9 +890,15 @@ flist_gen_dirent(struct sess *sess, char *root, struct flist **fl, size_t *sz,
>   * files and directory components, so use fts(3).
>   * Copying the information file-by-file into the flstat.
>   * We'll make sense of it in flist_send.
> + * If one-file-system is used, prevent fts(3) from descending
> + * into directories that have a different device number than
> + * the file from which the descent began.
>   */
>  
> - if ((fts = fts_open(cargv, FTS_PHYSICAL, NULL)) == NULL) {
> + if (sess->opts->one_file_system)
> + xdev = FTS_XDEV;
> +
> + if ((fts = fts_open(cargv, FTS_PHYSICAL | xdev, NULL)) == NULL) {
>   ERR(sess, "fts_open");
>   return 0;
>   }
> @@ -1129,7 +1135,7 @@ flist_gen_dels(struct sess *sess, const char *root, struct flist **fl,
>      size_t *sz, const struct flist *wfl, size_t wflsz)
>  {
>   char **cargv = NULL;
> - int  rc = 0, c;
> + int  rc = 0, c, xdev = 0;
>   FTS *fts = NULL;
>   FTSENT *ent;
>   struct flist *f;
> @@ -1235,9 +1241,15 @@ flist_gen_dels(struct sess *sess, const char *root, struct flist **fl,
>   * Now we're going to try to descend into all of the top-level
>   * directories stipulated by the file list.
>   * If the directories don't exist, it's ok.
> + * If one-file-system is used, prevent fts(3) from descending
> + * into directories that have a different device number than
> + * the file from which the descent began.
>   */
>  
> - if ((fts = fts_open(cargv, FTS_PHYSICAL, NULL)) == NULL) {
> + if (sess->opts->one_file_system)
> + xdev = FTS_XDEV;
> +
> + if ((fts = fts_open(cargv, FTS_PHYSICAL | xdev, NULL)) == NULL) {
>   ERR(sess, "fts_open");
>   goto out;
>   }
> diff --git usr.bin/rsync/main.c usr.bin/rsync/main.c
> index dd598d93eb2..3dbf6dd6028 100644
> --- usr.bin/rsync/main.c
> +++ usr.bin/rsync/main.c
> @@ -305,6 +305,7 @@ main(int argc, char *argv[])
>   { "no-times", no_argument, &opts.preserve_times, 0 },
>   { "verbose", no_argument, &opts.verbose, 1 },
>   { "no-verbose", no_argument, &opts.verbose, 0 },
> + { "one-file-system", no_argument, NULL, 'x' },
>   { NULL, 0, NULL, 0 }};
>  
>   /* Global pledge. */
> @@ -315,7 +316,7 @@ main(int argc, char *argv[])
>  
>   memset(&opts, 0, sizeof(struct opts));
>  
> - while ((c = getopt_long(argc, argv, "Dae:ghlnoprtvz", lopts, NULL))
> + while ((c = getopt_long(argc, argv, "Dae:ghlnoprtvxz", lopts, NULL))
>      != -1) {
>   switch (c) {
>   case 'D':
> @@ -359,6 +360,9 @@ main(int argc, char *argv[])
>   case 'v':
>   opts.verbose++;
>   break;
> + case 'x':
> + opts.one_file_system = 1;
> + break;
>   case 'z':
>   fprintf(stderr, "%s: -z not supported yet\n", getprogname());
>   break;
> diff --git usr.bin/rsync/rsync.1 usr.bin/rsync/rsync.1
> index c16168fff8f..911c848da7e 100644
> --- usr.bin/rsync/rsync.1
> +++ usr.bin/rsync/rsync.1
> @@ -141,6 +141,8 @@ Increase verbosity.
>  Specify once for files being transferred, twice for specific status,
>  thrice for per-file transfer information, and four times for per-file
>  breakdowns.
> +.It Fl x , -one-file-system
> +Do not cross filesystem boundaries.
>  .It Fl -version
>  Print version and exit.
>  .El
>

Reply | Threaded
Open this post in threaded view
|

Re: rsync: add --one-file-system

Theo de Raadt-2
In reply to this post by Björn Ketelaars
Looking at real rsync, it appears the 'x' option is passed to the
other side (your code does not do this), and there is also some
special case for >1 -x option.

Intuitively the client and server must do slightly different things
especially when combined with --delete, or if one of them finds a
fs-crossing in their space and the other doesn't, it gets weird.

Gut feel is you've missed something.

Reply | Threaded
Open this post in threaded view
|

Re: rsync: add --one-file-system

Florian Obser-2
In reply to this post by Björn Ketelaars
This does not do the right thing when sender and receiver do not agree
on file system boundaries.

Consider these mount points

/dev/sd1g on /rsync-test/dst type ffs (local)
/dev/sd1e on /rsync-test/src/e type ffs (local)
/dev/sd1f on /rsync-test/src/e/f type ffs (local)

and these files / directories:

drwxr-xr-x  3 root  wheel  512 Apr  2 10:39 src
drwxr-xr-x  3 root  wheel  512 Apr  2 10:39 src/e
-rw-r--r--  1 root  wheel    0 Apr  2 10:39 src/e/e-foo
drwxr-xr-x  2 root  wheel  512 Apr  2 10:39 src/e/f
-rw-r--r--  1 root  wheel    0 Apr  2 10:39 src/e/f/f-foo
drwxr-xr-x  3 root  wheel  512 Apr  2 10:38 dst
drwxr-xr-x  3 root  wheel  512 Apr  2 10:54 dst/e
drwxr-xr-x  2 root  wheel  512 Apr  2 10:54 dst/e/f
-rw-r--r--  1 root  wheel    0 Apr  2 10:54 dst/e/f/foo

$ rsync --delete -ax src/e/ dst/e/

results in

drwxr-xr-x  3 root  wheel  512 Apr  2 10:38 dst
drwxr-xr-x  3 root  wheel  512 Apr  2 10:39 dst/e
-rw-r--r--  1 root  wheel    0 Apr  2 10:39 dst/e/e-foo
drwxr-xr-x  2 root  wheel  512 Apr  2 10:39 dst/e/f
-rw-r--r--  1 root  wheel    0 Apr  2 10:54 dst/e/f/foo

$ openrsync --rsync-path=/usr/bin/openrsync  --delete -ax src/e/ dst/e/

results in

drwxr-xr-x  3 root  wheel  512 Apr  2 10:38 dst
drwxr-xr-x  3 root  wheel  512 Apr  2 10:39 dst/e
-rw-r--r--  1 root  wheel    0 Apr  2 10:39 dst/e/e-foo
drwxr-xr-x  2 root  wheel  512 Apr  2 10:39 dst/e/f

note how it decended into dst/e/f and deleted foo

On Mon, Apr 01, 2019 at 09:36:34PM +0200, Björn Ketelaars wrote:

> Add --one-file-system, which prevents openrsync to cross filesystem
> boundaries. Option and behaviour is the same as GPL rsync.
>
> OK?
>
>
> diff --git usr.bin/rsync/extern.h usr.bin/rsync/extern.h
> index 12aceae566c..602cd24cede 100644
> --- usr.bin/rsync/extern.h
> +++ usr.bin/rsync/extern.h
> @@ -117,6 +117,7 @@ struct opts {
>   int devices; /* --devices */
>   int specials; /* --specials */
>   int numeric_ids; /* --numeric-ids */
> + int one_file_system; /* --one-file-system or -x */
>   char *rsync_path; /* --rsync-path */
>   char *ssh_prog; /* --rsh or -e */
>   char *port; /* --port */
> diff --git usr.bin/rsync/flist.c usr.bin/rsync/flist.c
> index b39599583e3..7f18966d85d 100644
> --- usr.bin/rsync/flist.c
> +++ usr.bin/rsync/flist.c
> @@ -804,7 +804,7 @@ flist_gen_dirent(struct sess *sess, char *root, struct flist **fl, size_t *sz,
>      size_t *max)
>  {
>   char *cargv[2], *cp;
> - int rc = 0;
> + int rc = 0, xdev = 0;
>   FTS *fts;
>   FTSENT *ent;
>   struct flist *f;
> @@ -890,9 +890,15 @@ flist_gen_dirent(struct sess *sess, char *root, struct flist **fl, size_t *sz,
>   * files and directory components, so use fts(3).
>   * Copying the information file-by-file into the flstat.
>   * We'll make sense of it in flist_send.
> + * If one-file-system is used, prevent fts(3) from descending
> + * into directories that have a different device number than
> + * the file from which the descent began.
>   */
>  
> - if ((fts = fts_open(cargv, FTS_PHYSICAL, NULL)) == NULL) {
> + if (sess->opts->one_file_system)
> + xdev = FTS_XDEV;
> +
> + if ((fts = fts_open(cargv, FTS_PHYSICAL | xdev, NULL)) == NULL) {
>   ERR(sess, "fts_open");
>   return 0;
>   }
> @@ -1129,7 +1135,7 @@ flist_gen_dels(struct sess *sess, const char *root, struct flist **fl,
>      size_t *sz, const struct flist *wfl, size_t wflsz)
>  {
>   char **cargv = NULL;
> - int  rc = 0, c;
> + int  rc = 0, c, xdev = 0;
>   FTS *fts = NULL;
>   FTSENT *ent;
>   struct flist *f;
> @@ -1235,9 +1241,15 @@ flist_gen_dels(struct sess *sess, const char *root, struct flist **fl,
>   * Now we're going to try to descend into all of the top-level
>   * directories stipulated by the file list.
>   * If the directories don't exist, it's ok.
> + * If one-file-system is used, prevent fts(3) from descending
> + * into directories that have a different device number than
> + * the file from which the descent began.
>   */
>  
> - if ((fts = fts_open(cargv, FTS_PHYSICAL, NULL)) == NULL) {
> + if (sess->opts->one_file_system)
> + xdev = FTS_XDEV;
> +
> + if ((fts = fts_open(cargv, FTS_PHYSICAL | xdev, NULL)) == NULL) {
>   ERR(sess, "fts_open");
>   goto out;
>   }
> diff --git usr.bin/rsync/main.c usr.bin/rsync/main.c
> index dd598d93eb2..3dbf6dd6028 100644
> --- usr.bin/rsync/main.c
> +++ usr.bin/rsync/main.c
> @@ -305,6 +305,7 @@ main(int argc, char *argv[])
>   { "no-times", no_argument, &opts.preserve_times, 0 },
>   { "verbose", no_argument, &opts.verbose, 1 },
>   { "no-verbose", no_argument, &opts.verbose, 0 },
> + { "one-file-system", no_argument, NULL, 'x' },
>   { NULL, 0, NULL, 0 }};
>  
>   /* Global pledge. */
> @@ -315,7 +316,7 @@ main(int argc, char *argv[])
>  
>   memset(&opts, 0, sizeof(struct opts));
>  
> - while ((c = getopt_long(argc, argv, "Dae:ghlnoprtvz", lopts, NULL))
> + while ((c = getopt_long(argc, argv, "Dae:ghlnoprtvxz", lopts, NULL))
>      != -1) {
>   switch (c) {
>   case 'D':
> @@ -359,6 +360,9 @@ main(int argc, char *argv[])
>   case 'v':
>   opts.verbose++;
>   break;
> + case 'x':
> + opts.one_file_system = 1;
> + break;
>   case 'z':
>   fprintf(stderr, "%s: -z not supported yet\n", getprogname());
>   break;
> diff --git usr.bin/rsync/rsync.1 usr.bin/rsync/rsync.1
> index c16168fff8f..911c848da7e 100644
> --- usr.bin/rsync/rsync.1
> +++ usr.bin/rsync/rsync.1
> @@ -141,6 +141,8 @@ Increase verbosity.
>  Specify once for files being transferred, twice for specific status,
>  thrice for per-file transfer information, and four times for per-file
>  breakdowns.
> +.It Fl x , -one-file-system
> +Do not cross filesystem boundaries.
>  .It Fl -version
>  Print version and exit.
>  .El
>


--
I'm not entirely sure you are real.

Reply | Threaded
Open this post in threaded view
|

Re: rsync: add --one-file-system

Björn Ketelaars
In reply to this post by Björn Ketelaars
On Mon 01/04/2019 21:36, Björn Ketelaars wrote:
> Add --one-file-system, which prevents openrsync to cross filesystem
> boundaries. Option and behaviour is the same as GPL rsync.

Thanks for all the feedback!

New diff:
- Without the long option;
- Appropriate changes to usage and rsync.1;
- '-x' / '-xx' is passed to the other side;
- Special case for >1 '-x' option has been addressed;
- More important, permit fts to walk the whole filesystem and use dev_t
  comparison.

Florian's proposed scenario seems to work, as do several variants of
'-x' / '-xx' and '--delete'.

Comments?


diff --git usr.bin/rsync/extern.h usr.bin/rsync/extern.h
index 9ae08e7515e..038997aa164 100644
--- usr.bin/rsync/extern.h
+++ usr.bin/rsync/extern.h
@@ -117,6 +117,7 @@ struct opts {
  int devices; /* --devices */
  int specials; /* --specials */
  int numeric_ids; /* --numeric-ids */
+ int one_file_system; /* -x */
  char *rsync_path; /* --rsync-path */
  char *ssh_prog; /* --rsh or -e */
  char *port; /* --port */
diff --git usr.bin/rsync/fargs.c usr.bin/rsync/fargs.c
index 55bf1b4b8af..a185d0c6a5d 100644
--- usr.bin/rsync/fargs.c
+++ usr.bin/rsync/fargs.c
@@ -106,6 +106,10 @@ fargs_cmdline(struct sess *sess, const struct fargs *f, size_t *skip)
  addargs(&args, "-v");
  if (sess->opts->verbose > 0)
  addargs(&args, "-v");
+ if (sess->opts->one_file_system)
+ addargs(&args, "-x");
+ if (sess->opts->one_file_system > 1)
+ addargs(&args, "-x");
  if (sess->opts->specials && !sess->opts->devices)
  addargs(&args, "--specials");
  if (!sess->opts->specials && sess->opts->devices)
diff --git usr.bin/rsync/flist.c usr.bin/rsync/flist.c
index b39599583e3..a3d1f338b74 100644
--- usr.bin/rsync/flist.c
+++ usr.bin/rsync/flist.c
@@ -804,11 +804,12 @@ flist_gen_dirent(struct sess *sess, char *root, struct flist **fl, size_t *sz,
     size_t *max)
 {
  char *cargv[2], *cp;
- int rc = 0;
+ int rc = 0, nxdev = 0, flag, i;
  FTS *fts;
  FTSENT *ent;
  struct flist *f;
  size_t flsz = 0, stripdir;
+ dev_t *xdev;
  struct stat st;
 
  cargv[0] = root;
@@ -913,6 +914,48 @@ flist_gen_dirent(struct sess *sess, char *root, struct flist **fl, size_t *sz,
  continue;
  }
 
+ /*
+ * If rsync is told to avoid crossing a filesystem
+ * boundary when recursing, then replace all mount point
+ * directories with empty directories.  The latter is
+ * prevented by telling rsync multiple times to avoid
+ * crossing a filesystem boundary when recursing.
+ * Replacing mount point directories is tricky. We need
+ * to sort out which directories to include.  As such,
+ * keep track of unique device inodes, and use these for
+ * comparison.
+ */
+
+ if (sess->opts->one_file_system &&
+    ent->fts_statp->st_dev != st.st_dev) {
+ if (sess->opts->one_file_system > 1 ||
+    !S_ISDIR(ent->fts_statp->st_mode))
+ continue;
+
+ if ((xdev = malloc(sizeof(dev_t))) == NULL) {
+ ERRX1(sess, "malloc");
+ goto out;
+ }
+
+ flag = 0;
+ for (i = 0; i < nxdev; i++)
+ if (xdev[i] == ent->fts_statp->st_dev) {
+ flag = 1;
+ break;
+ }
+ if (flag)
+ continue;
+
+ if (nxdev)
+ if ((xdev = realloc(xdev, sizeof(dev_t))) ==
+    NULL) {
+ ERRX1(sess, "realloc");
+ goto out;
+ }
+ xdev[nxdev] = ent->fts_statp->st_dev;
+ nxdev++;
+ }
+
  /* Allocate a new file entry. */
 
  if (!flist_realloc(sess, fl, sz, max)) {
@@ -966,6 +1009,8 @@ flist_gen_dirent(struct sess *sess, char *root, struct flist **fl, size_t *sz,
  rc = 1;
 out:
  fts_close(fts);
+ if (sess->opts->one_file_system)
+ free(xdev);
  return rc;
 }
 
@@ -1129,10 +1174,11 @@ flist_gen_dels(struct sess *sess, const char *root, struct flist **fl,
     size_t *sz, const struct flist *wfl, size_t wflsz)
 {
  char **cargv = NULL;
- int  rc = 0, c;
+ int  rc = 0, c, flag;
  FTS *fts = NULL;
  FTSENT *ent;
  struct flist *f;
+ struct stat  st;
  size_t  cargvs = 0, i, j, max = 0, stripdir;
  ENTRY  hent;
  ENTRY *hentp;
@@ -1253,6 +1299,31 @@ flist_gen_dels(struct sess *sess, const char *root, struct flist **fl,
  } else if (stripdir >= ent->fts_pathlen)
  continue;
 
+ assert(ent->fts_statp != NULL);
+
+ /*
+ * If rsync is told to avoid crossing a filesystem
+ * boundary when recursing, then exclude all entries
+ * from the list with a device inode, which does not
+ * match that of one of the top-level directories.
+ */
+
+ if (sess->opts->one_file_system) {
+ flag = 0;
+ for (i = 0; i < wflsz; i++) {
+ if (stat(wfl[i].path, &st) == -1) {
+ ERR(sess, "%s: stat", wfl[i].path);
+ goto out;
+ }
+ if (ent->fts_statp->st_dev == st.st_dev) {
+ flag = 1;
+ break;
+ }
+ }
+ if (!flag)
+ continue;
+ }
+
  /* Look up in hashtable. */
 
  memset(&hent, 0, sizeof(ENTRY));
@@ -1273,7 +1344,6 @@ flist_gen_dels(struct sess *sess, const char *root, struct flist **fl,
  goto out;
  }
  f->wpath = f->path + stripdir;
- assert(ent->fts_statp != NULL);
  flist_copy_stat(f, ent->fts_statp);
  errno = 0;
  }
diff --git usr.bin/rsync/main.c usr.bin/rsync/main.c
index dd598d93eb2..53c42351789 100644
--- usr.bin/rsync/main.c
+++ usr.bin/rsync/main.c
@@ -315,7 +315,7 @@ main(int argc, char *argv[])
 
  memset(&opts, 0, sizeof(struct opts));
 
- while ((c = getopt_long(argc, argv, "Dae:ghlnoprtvz", lopts, NULL))
+ while ((c = getopt_long(argc, argv, "Dae:ghlnoprtvxz", lopts, NULL))
     != -1) {
  switch (c) {
  case 'D':
@@ -359,6 +359,9 @@ main(int argc, char *argv[])
  case 'v':
  opts.verbose++;
  break;
+ case 'x':
+ opts.one_file_system++;
+ break;
  case 'z':
  fprintf(stderr, "%s: -z not supported yet\n", getprogname());
  break;
@@ -505,7 +508,7 @@ main(int argc, char *argv[])
  exit(rc);
 usage:
  fprintf(stderr, "usage: %s"
-    " [-aDglnoprtv] [-e program] [--del] [--numeric-ids]\n"
+    " [-aDglnoprtvx] [-e program] [--del] [--numeric-ids]\n"
     "\t[--port=portnumber] [--rsync-path=program] [--version]\n"
     "\tsource ... directory\n",
     getprogname());
diff --git usr.bin/rsync/rsync.1 usr.bin/rsync/rsync.1
index c16168fff8f..cd1513dc89a 100644
--- usr.bin/rsync/rsync.1
+++ usr.bin/rsync/rsync.1
@@ -22,7 +22,7 @@
 .Nd synchronise local and remote files
 .Sh SYNOPSIS
 .Nm openrsync
-.Op Fl aDglnoprtv
+.Op Fl aDglnoprtvx
 .Op Fl e Ar program
 .Op Fl -del
 .Op Fl -numeric-ids
@@ -141,6 +141,11 @@ Increase verbosity.
 Specify once for files being transferred, twice for specific status,
 thrice for per-file transfer information, and four times for per-file
 breakdowns.
+.It Fl x
+Do not cross filesystem boundaries.
+If this option is repeated, all mount point directories from the copy are
+omitted.
+Otherwise, it includes an empty directory at each mount point it encounters.
 .It Fl -version
 Print version and exit.
 .El