Quick start

Two machines are involved: the server, which holds the master, and each client, which holds a copy. Install App::FileSync on both — filesync-admin and filesync-server run on the server, filesync on the clients.

On this page

On the server

Create the repo as the account the clients log in to. A session over ssh carries only the repo, the client id and the role, so the client never tells the server which root to use: name the same --root on every server command.

# on the server, once
filesync-admin --root ~/filesync repo create documents
filesync-admin --root ~/filesync client allow laptop --repo documents

repo create makes the master directory and the metadata beside it, printing both paths. client allow is optional, because a repo with no clients admits every client; it takes the client's id, which is the host name unless --id was given.

The example keeps the master in the server account's home directory, so nothing has to run as root. /srv/filesync is the built-in default root; see a root of /srv/filesync instead if you prefer it.

The server-command wrapper

The server also needs --root when filesync-server starts, and the client cannot pass one, so wrap it:

# on the server, once
cat > ~/bin/filesync-server-home <<'EOF'
#!/bin/sh
exec /usr/local/bin/filesync-server --root "$HOME/filesync" "$@"
EOF
chmod +x ~/bin/filesync-server-home

Use the path filesync-server was actually installed to, which ssh srv 'command -v filesync-server' shows: a non-interactive ssh command can see a shorter PATH than an interactive login shell.

On each client

Register the directory and name the wrapper as the server program:

# on the client, once
filesync init --repo documents --root /home/foo/Documents \
        --server foo@srv \
        --server-command /home/foo/bin/filesync-server-home

Nothing else needs naming: the client reports its host name, which is the id client allow lists and the name the server keeps this client's base, its journal entries and its conflict copies under. Pass --id only when the host name is not the name you want, and allow that name on the server instead. An id is not a login — the ssh key and the account do the authenticating — and it does not divide the repo: every client of a repo syncs the whole tree.

ssh runs with BatchMode=yes, so the client's key must already be in the server account's authorized_keys, or loaded in ssh-agent: cron cannot answer a password prompt.

The first sync

Look before you leap. --dry-run prints the actions the session would take without changing either side.

filesync sync --repo documents --dry-run
filesync sync --repo documents

Each run reports what moved and how much of it crossed the wire:

documents: pulled 0, pushed 3, deleted 0, conflicts 0
  transfer: sent 148.2 KiB, received 0 B, 1 file by delta

From cron

# crontab -e, on each client
* * * * * ionice -c3 /usr/local/bin/filesync sync --repo documents --quiet >/dev/null

The summary line is dropped so cron does not mail on every successful run; diagnostics still go to standard error, so a failure is mailed. Every attempt, successful or not, is also appended to ~/.filesync/logs/documents.jsonl.

Adjust /usr/local/bin/filesync to where the script was installed, because cron's default PATH rarely includes a per-user Perl bin directory.

Checking what happened

filesync status --repo documents
filesync log --repo documents -n 5
filesync log --repo documents --exit 1
filesync verify --repo documents --deep

status shows when each repo last synced, how big it is and what the last transfer cost. log filters the run log by time, exit code or warning. verify compares the local tree with the base the last successful sync recorded; it works offline.

Exit codes tell cron what happened: 0 ok, 1 fatal, 2 another run holds the lock, 3 completed with warnings, 4 verification found drift.

On Windows

Windows clients use a scheduled task rather than cron. With the script in C:\filesync and Strawberry Perl installed, the same job is set up from a command prompt:

REM on the client, once
C:\Strawberry\perl\bin\perl.exe C:\filesync\bin\filesync init ^
  --repo documents --root C:\Users\foo\Documents --server foo@srv ^
  --server-command /home/foo/bin/filesync-server-home

REM then create the task; it runs every minute as the current user
schtasks /Create /SC MINUTE /MO 1 /TN FileSync /F ^
  /TR "C:\Strawberry\perl\bin\perl.exe C:\filesync\bin\filesync sync --repo documents --quiet"

The task runs only while that user is logged on, which suits a Documents folder; its run log is still written under %USERPROFILE%\.filesync\logs, so filesync log --repo documents --warnings works there too.

A root of /srv/filesync instead

To keep the master outside a home directory, create the default root once for the account the clients log in to. Then no --root is needed on the server commands, and no --server-command on the client:

# on the server, as an administrator
sudo mkdir -p /srv/filesync
sudo chown -R foo /srv/filesync

filesync-admin repo create documents then works as it is, and the master is /srv/filesync/master/documents.

Backup and restore

The master directory is an ordinary directory tree, so it can be mirrored to a backup directory from the server account's crontab. Make the backup directory once — rsync creates the last component only — then let cron keep it in step:

# on the server, once
mkdir -p /home/foo/backups/documents

# crontab -e, on the server
* * * * * ionice -c3 rsync -a --delete --exclude '.filesync-tmp-*' \
    /home/foo/filesync/master/documents/ /home/foo/backups/documents/

Filesync writes each file to a temporary name and renames it into place, so the mirror never catches a half-written upload; the exclusion keeps the temporary files themselves out of the backup. --delete makes the copy a mirror: a deletion that reaches the master also removes the backed-up file.

To restore the master from that backup, stop the client jobs (comment out their cron lines, or disable the scheduled task) and the server's backup job, so the backup job cannot copy a half-restored tree back into the backup. Let any running sync finish, then mirror the backup back:

rsync -a --delete --checksum \
    /home/foo/backups/documents/ /home/foo/filesync/master/documents/

--checksum compares the contents, so a restored file is not skipped just because its size and modification time happen to match the newer copy. This is a rollback rather than a merge: files created after the backup was taken disappear from the master, and the next sync propagates that to the clients, so copy anything you need to keep out of the tree first.

The server would rehash the restored files on the next session anyway, but filesync-admin reindex rebuilds the index immediately, before any client reconnects; clients then pull the restored versions, merging edits they have not synced as usual.

Before re-enabling the jobs, check that the copy is exact with a read-only comparison against the backup:

rsync -a --delete --checksum --dry-run --itemize-changes \
    /home/foo/backups/documents/ /home/foo/filesync/master/documents/

No output and exit 0 mean the master matches the backup. Any line beginning >f names a path that still differs, and *deleting names one the rollback should have removed. The index still describes the pre-rollback tree, so filesync-admin verify may report drift at this point; that is expected. Rebuild it and check it:

filesync-admin --root ~/filesync reindex documents
filesync-admin --root ~/filesync verify documents --deep

reindex hashes every file in the master and replaces the index atomically; it refuses to run while a sync session holds the repo lock, and it names any paths it could not index. clean and exit 0 from verify confirm that the master and its index agree. Re-enable the jobs once that check passes.

To restore just one file or directory, copy that path back instead of mirroring the whole tree, with the jobs paused as above so nothing changes the tree while the copy runs:

rsync -a --checksum /home/foo/backups/documents/notes/plan.txt \
    /home/foo/filesync/master/documents/notes/
rsync -a --checksum /home/foo/backups/documents/photos/ \
    /home/foo/filesync/master/documents/photos/

The trailing slash on the second source copies the directory's contents; without it rsync would put the directory itself inside the destination. Add --delete only when that directory should match the backup exactly, since it removes files created after the backup was taken. Refresh the index with reindex as above, or let the next session do it, and clients pull the restored versions.