rsync is great at mirroring a tree and terrible at answering "only the files changed in the last 7 days". It has size filters, name filters, and include/exclude rules, but no native "modified in the last N days" expression. find has exactly that. The standard move is to let find select the files and hand the list to rsync:
find . -type f -mtime -7 -print0 | rsync -av --files-from=- --from0 ./ user@host:/dest/That syncs exactly the files find matched, nothing else. This page is the breakdown of why each flag is there, the one path mismatch that breaks it every time, and when you should skip find because rsync can do the selection itself.
Set your values
Set your OS, source path, the day window, and the rsync destination. Every command below updates with your values.
The one-liner
find :search_path -type f -mtime -:days -print0 | rsync -avn --files-from=- --from0 :search_path :destNote the n in -avn: that is a dry run. It prints what rsync would copy without copying anything. Run the command exactly like this first, read the list, then drop the n to do the real transfer. More on that below, because for any rsync to a remote it is not optional.
Windows has no native rsync. Use WSL (which gives you the real GNU find and rsync), or fall back to robocopy with its /MAXAGE switch, which is the closest built-in equivalent for an age-based selective copy.
Breaking down the flags
The pipeline has two halves. find produces a list of paths; rsync consumes it.
| Piece | What it does |
|---|---|
find . -type f -mtime -7 | Selects regular files modified in the last 7 days |
-print0 | Prints each path terminated by a NUL byte, not a newline |
| | Pipes that list into rsync's stdin |
--files-from=- | Tells rsync to read the file list from a file; - means stdin |
--from0 | Tells rsync the list is NUL-delimited (pairs with -print0) |
-a | Archive mode: recurse, preserve perms, times, symlinks, owners |
-v | Verbose: print each file as it transfers |
./ | The source argument: rsync resolves list paths relative to this |
user@host:/dest/ | The destination |
--files-from=- is the key. Normally rsync takes a source path and decides for itself what to send. With --files-from, you give it an explicit list and rsync transfers only those entries. The - is the conventional "read from stdin" marker, same as it means to tar or cat.
--from0 exists because filenames on Unix can contain newlines. A plain newline-delimited list breaks the instant a filename has a newline in it. find -print0 emits NUL separators, and --from0 tells rsync to split on NUL. The two flags are a matched pair: use -print0 on the find side, --from0 on the rsync side. Use both or neither.
The gotcha that breaks it every time
Paths in --files-from are relative to the rsync source argument, not relative to your shell's current directory and not absolute.
Walk through what happens. find . -type f prints paths like ./logs/app.log. rsync reads ./logs/app.log from the list. The source argument is ./. So rsync looks for ./logs/app.log under ./, finds it, and transfers it. That works because find's start path (.) and rsync's source (./) agree.
Now break it. Suppose you run find /var/www -type f -mtime -7 -print0 but pass ./ as the rsync source. find prints absolute paths like /var/www/index.php. rsync joins that onto ./ and looks for ././var/www/index.php, which does not exist. rsync prints link_stat ... failed: No such file or directory for every entry and copies nothing.
The rule:
- If
findprints./relative/paths, the rsync source must be./(or.). - If
findprints absolute paths (find /var/www ...), the rsync source must be/and the destination receives the full/var/www/...tree underneath it. Usually not what you want. - The clean pattern:
cdinto the directory first, runfind ., and use./as the source. Then the list paths and the source agree.
This single mismatch is the number one reason "find piped to rsync" appears to do nothing. If rsync transfers zero files and prints failed: No such file or directory, the source-vs-list-path relationship is wrong.
Sync files modified in the last N days
The headline use case. rsync cannot express a time window, find does it with -mtime:
cd :search_path
find . -type f -mtime -:days -print0 | rsync -avn --files-from=- --from0 ./ :dest-mtime -7 means "modified less than 7 days ago". For an hour-resolution window use -mmin -60. The full -mtime and -mmin reference, including the off-by-one rounding rule, is in Find files modified in the last 7 days.
Sync only a certain file type modified since a date
Combine a name test with a time test. Here, only .sql dumps written since a marker time:
touch -t 202604010000 /tmp/since
cd :search_path
find . -type f -name '*.sql' -newer /tmp/since -print0 | rsync -avn --files-from=- --from0 ./ :dest-newer FILE compares against the modification time of a reference file, which you set with touch -t YYYYMMDDhhmm. It is more precise than -mtime because it skips the day-floor rounding. This is the pattern for "sync everything that changed since the last backup ran": touch a marker at the end of each run, and the next run uses it as the -newer reference.
Sync a curated list of files
--files-from does not care where the list came from. If you already have a hand-written manifest, point rsync straight at it, no find and no --from0 (a hand-written file is newline-delimited):
rsync -avn --files-from=manifest.txt ./ user@host:/dest/Each line of manifest.txt is one path relative to ./. This is handy for deploy scripts that ship a fixed set of files, or for re-running a transfer that failed partway: capture the remaining paths into a file and feed it back.
When you do NOT need find
rsync has a real filter language. If the selection is something rsync can express on its own, the pipeline is just extra moving parts. rsync alone handles:
- By name or extension:
rsync -av --include='*.log' --exclude='*' ./ dest/syncs only.logfiles. The trailing--exclude='*'matters: without it, everything else also goes. - By size:
--max-size=1Mand--min-size=10kcap the transfer by file size. - Excluding a subtree:
--exclude='node_modules/'skips a directory entirely. - Only files that differ: that is rsync's default. It already skips files whose size and mtime match the destination. You do not need
findto "only sync changed files"; rsync does that for you.
find earns its place for two things rsync genuinely cannot do:
- Time windows. rsync has no "modified in the last N days" filter.
find -mtime/-mmin/-neweris the only clean way. - Complex multi-condition selection. "Files over 1MB, owned by
www-data, not modified in 30 days, but not undercache/" is afindexpression. Stacking that many rsync filters is unreadable and error-prone.
Rule of thumb: if you can describe the selection with name, size, or path alone, use rsync filters. If the word "when" or "modified" appears, reach for find.
Always dry-run first
rsync -avn (lowercase n, the long form is --dry-run) walks the whole transfer and prints exactly what it would do, transferring nothing. For any rsync to a remote host, run the dry run first, every time, no exceptions:
cd :search_path
find . -type f -mtime -:days -print0 | rsync -avn --files-from=- --from0 ./ :dest
# read the output, confirm it is what you expect, then drop the n:
find . -type f -mtime -:days -print0 | rsync -av --files-from=- --from0 ./ :destThe dry run catches the path-mismatch bug, catches a wrong destination, catches a find expression that selected more than you thought. It costs one extra command and saves you from writing the wrong files to a remote server. The danger compounds with --delete: a real run with --delete and a bad source can wipe files on the destination. Dry-run anything with --delete in it without exception.
macOS BSD vs GNU rsync
The rsync that ships with macOS has a complicated history, and it matters for this pipeline.
| Era | What macOS shipped | --files-from / --from0 |
|---|---|---|
| macOS up to Catalina | GNU-derived rsync, frozen at 2.6.9 (2006) | Supported, but ancient and buggy |
| Sonoma onward | openrsync (a BSD-licensed rewrite) | --files-from supported; --from0 support is newer and partial |
| Homebrew (any macOS) | Current upstream rsync 3.x | Full support |
The frozen 2.6.9 is the real trap: it predates a decade of fixes and its --files-from handling has edge cases. openrsync is modern but is a separate implementation, so flag coverage can lag. On macOS I install current rsync rather than fight either default:
brew install rsyncConfirm which one you are calling with rsync --version. Upstream rsync reports version 3.x.x; openrsync identifies itself as openrsync. On Linux, the distro rsync is current upstream and all the flags here work as written.
The find side has its own BSD/GNU split: macOS find lacks -printf, but -print0, -mtime, -mmin, and -newer all work on both. The full divergence table is in the find Command Cheat Sheet.
Common mistakes
1. Source path does not match the list paths. Covered above and worth repeating because it is the dominant failure. find . prints ./relative paths, so the rsync source must be ./. find /abs/path prints absolute paths, so the source must be /. Mismatch and rsync copies nothing.
2. Forgetting --from0 after using -print0. -print0 emits NUL-delimited output. Without --from0, rsync tries to split that on newlines, sees one giant "filename" containing embedded NULs, and fails. Either pair -print0 with --from0, or use plain -print (newline) with no --from0 and accept the broken-on-newline-filenames risk.
3. Skipping the dry run. rsync -av to a remote with no prior -avn is how you discover the path bug by writing garbage to a server. Always dry-run.
4. Trailing slash confusion on the source. With --files-from, the source is a base directory and the trailing slash is far less load-bearing than in a normal rsync src/ dest/ invocation, because the list paths carry the structure. But keep the source consistent between the dry run and the real run. Changing ./ to . between runs is fine; changing it to subdir/ is not.
5. Expecting --files-from to create missing parent directories. rsync recreates the directory structure of the listed paths under the destination by default in archive mode. If you see files landing flat with no subdirs, check that you are in archive mode (-a) and not stripping path components.
6. Using this as a backup strategy. A selective rsync is a transfer, not a backup. It has no versioning, no retention, no integrity verification across runs. For actual backups, use a real backup tool. The off-server backups article is the reference for that.
When NOT to use this pattern
- The selection is simple. "All
.logfiles" or "everything under 1MB" is a job for rsync's own--include/--exclude/--max-size. Skipfind. - You want a full mirror. If you are syncing an entire tree, plain
rsync -av src/ dest/is simpler and lets rsync's own change detection do the work.--files-fromis for partial transfers. - You need a backup, not a copy. Selective rsync gives you no history. Use a tool built for backups; see off-server backups.
- The file set changes mid-transfer. Piping
findinto rsync snapshots the list at pipeline start. If files are being written while the transfer runs, the list can be stale. For volatile directories, snapshot the filesystem first or accept the inconsistency.
See also
- find Command Cheat Sheet: the full find reference covering name, type, size, time, and exec patterns
- Find files modified in the last 7 days: the
-mtime/-mmin/-newerreference behind the time filters above - Find and tar an archive: the same find-selects-files idea, but bundling into a tarball instead of syncing
- Off-server backups: when you need real backups, not a selective copy
- External: rsync(1) man page, GNU findutils manual.





