I wanted a simple setup: my PC syncs files to my Synology NAS in real-time, one-way, no deletions. How hard could it be?
Turns out: hard enough that I built a tool to generate the setup script automatically - nas-sync-script-builder - because there were too many moving parts to get right by hand.
Here is what was not working, and how I fixed each one.
1. lsyncd started before my drives were even mounted
My initial setup used the drives as they were automatically mounted by Dolphin (KDE file manager) at /media/<user>/<partition-label>. Made sense - the drives show up in Dolphin, they're mounted, done.
lsyncd didn't agree.
The problem is where in the boot sequence each mechanism fires:
local-fs.target ← /etc/fstab mounts happen here
↓
multi-user.target ← lsyncd.service starts here
↓
graphical.target
↓
KDE session starts
↓
udisks2 + Dolphin → /media/<user>/<label> ← drives mounted HERE
lsyncd is a system service. It starts at multi-user.target, long before any user session exists. Dolphin uses udisks2, which only mounts drives after a user logs in to a graphical session, via D-Bus. By the time Dolphin mounts /media/<user>/<partition>, lsyncd has already tried to watch a path that didn't exist.
The fix: use /etc/fstab for local drive mounts.
fstab entries are processed by systemd-fstab-generator at local-fs.target - well before multi-user.target. The drives are available when lsyncd needs them.
# /etc/fstab
LABEL=<partition> /mnt/data/<partition> ntfs3 uid=1000,gid=1000,nofail 0 0
Simple, but only obvious once you understand the boot sequence.
2. My primary partition filled up completely after a power outage
This was the worst one.
The setup uses lsyncd to watch local drives and rsync changes to the NAS in real-time. The NAS is mounted via CIFS at /mnt/nas/.... Everything works beautifully - until there's a power outage.
When the power came back, my PC booted up. The NAS did not.
lsyncd started anyway. The NAS mount points existed as empty directories on the local drive. The CIFS shares were not mounted into them - they were just empty folders. So lsyncd started syncing all partitions into those empty local directories, which happened to be on my primary partition.
Within minutes, the primary partition was full.
The fix: BindsTo= in the systemd service override.
# /etc/systemd/system/lsyncd.service.d/override.conf
[Unit]
After=local-fs.target remote-fs.target network-online.target mnt-nas-<share>.mount
RequiresMountsFor=/mnt/data/<partition-a> /mnt/data/<partition-b>
Requires=mnt-nas-<share>.mount
BindsTo=mnt-nas-<share>.mount
[Service]
Restart=on-failure
RestartSec=10
The four directives each do a different job:
-
After=- don't start lsyncd until network and all mounts are up -
RequiresMountsFor=- local drives must be mounted first -
Requires=- declare dependency on NAS mount units -
BindsTo=- if a NAS mount drops mid-run, stop lsyncd immediately
Without these, if the NAS isn't up at boot, lsyncd starts anyway and syncs into empty directories. With these, lsyncd refuses to start unless all NAS mounts are active, and stops the moment any of them drops.
After=local-fs.target remote-fs.target network-online.target is not enough.
3. rsync re-uploaded entire folders on every restart
After initial setup, rsync kept re-uploading files it had already synced. Not all of them - seemingly random folders. Gigabytes, on every restart.
The culprit: CIFS timestamp caching.
rsync uploads to a temporary file with a random extension, then renames it to the final name on completion. The NAS sees this final step as a rename operation, and the CIFS client caches the rename timestamp as the file's mtime rather than the original modification time. So the next time rsync compares source and destination timestamps, it sees a mismatch that doesn't reflect any content change, and re-uploads the file.
--size-only in rsync fixes this:
rsync -a --update --size-only --no-perms --info=progress2 "$SRC" "$DST"
--size-only tells rsync to skip files where the size matches, regardless of timestamps. Once you've been burned by phantom re-uploads, you add noac to the CIFS mount options as well - it disables attribute caching on the client so every stat call goes to the server:
# /etc/fstab (NAS CIFS mount)
//<nas-hostname>/<share> /mnt/nas/<share> cifs credentials=/etc/samba/credentials,...,noac 0 0
--no-perms is also worth adding - CIFS ignores Unix permissions anyway, so rsync would always see a mismatch there too.
When something has already cost you gigabytes of redundant uploads, redundant protection is the right call.
4. lsyncd started syncing downloads the moment they began
lsyncd watches for filesystem events and triggers rsync almost immediately. This is exactly what you want - unless what appeared on the filesystem is a torrent file that just started downloading.
The moment qBittorrent created the file, lsyncd saw the inotify event and started uploading it to the NAS. A partially downloaded, actively written file. The upload saturates the network. And if you try to browse the download folder in Dolphin while this is happening, it won't even open - the IO traffic makes it completely unresponsive.
The fix: exclude partial download extensions.
EXCLUDE_ITEMS=(
'*.part'
'*.crdownload'
'*.!qb'
)
The *.!qb extension requires one additional step: in qBittorrent settings, enable "Append .!qb extension to incomplete files". Without this, qBittorrent uses the real filename even while downloading, and there's no pattern to exclude.
Chrome uses .crdownload. Firefox uses .part. Most download managers have something similar. Check your tools.
5. The Lua mount check
Even with BindsTo= in the systemd unit, I added a runtime check inside lsyncd.conf.lua:
local function is_mounted(path)
local f = io.popen("mountpoint -q " .. path .. " && echo yes || echo no")
local result = f:read("*l")
f:close()
return result == "yes"
end
if not is_mounted("/mnt/nas/<share>") then
error("NAS mount not available: /mnt/nas/<share>")
end
systemd's BindsTo= operates at service start and on unit deactivation events. But there are edge cases - network hiccups, stale mounts - where the mountpoint directory exists and the systemd unit looks active, but the share is no longer actually mounted. The Lua check runs at lsyncd startup and catches these cases before any sync happens.
After the power outage incident, I wasn't taking any chances.
The tool
After going through all of this, I wrapped the setup into a Python GUI that auto-detects local partitions via UDisks2 D-Bus, lets you configure the NAS connection and exclude patterns, and generates an idempotent bash script that handles all of the above correctly.
Install:
pip install nas-sync-script-builder
Run:
nas-sync-script-builder
The GUI detects your partitions, you fill in the NAS hostname and username, click Generate, and you get a nas-sync.sh that you run once with sudo. It's safe to re-run - all fstab entries are wrapped in marker comments and replaced cleanly on each run.
- GitHub: https://github.com/Jinjinov/nas-sync-script-builder
- PyPI: https://pypi.org/project/nas-sync-script-builder/
Summary
| Problem | Root cause | Fix |
|---|---|---|
| lsyncd started before drives were ready | udisks2/Dolphin mounts happen after graphical session | fstab mounts at local-fs.target
|
| Primary partition filled up after power outage | NAS not up at boot, lsyncd synced into empty mountpoints |
BindsTo= in systemd override |
| Files re-uploaded on every restart | CIFS caches stale timestamps after renames |
--size-only in rsync + noac mount option |
| Downloads synced to NAS while still in progress | inotify fires the moment a file is created | Exclude *.part, *.!qb, etc. + qBittorrent setting |
| Stale mounts not caught at runtime | systemd events miss some edge cases | Lua is_mounted() check in lsyncd config |
Every one of them came from something actually breaking.
Top comments (0)