usm azsync¶
Watch a local directory and keep it mirrored to Azure Blob Storage. Think
rsync -avuP that never exits: it batches changes, transfers them with
azcopy sync, and rotates the SAS token underneath you.
usm azsync add ./data https://acct.blob.core.windows.net/bucket/data
usm azsync ls
usm azsync status data-bucket
Why not just loop usm cp¶
usm cp is a one-shot transfer. azsync adds the three things a long-lived
mirror needs:
- Batching — a build that rewrites 5 000 files must not launch 5 000 transfers, and a single saved file must not wait an hour.
- Credential rotation —
azcopybakes the SAS into the job at start and cannot rotate it mid-flight, so something has to mint a fresh one between transfers and recover when one expires anyway. - Supervision — a daemon, restart-on-failure, and boot integration.
When it transfers¶
The trigger policy is the heart of the command. A transfer fires on the first of these:
| Trigger | Default | Meaning |
|---|---|---|
| quiet period | --quiet-period 5 |
Writes stopped for 5s — the normal path. |
| volume | --batch-files 200 / --batch-bytes 256MiB |
Enough changed; go now, don't wait for quiet. |
| max delay | --max-delay 300 |
Something is always writing; cap the staleness. |
| heartbeat | --interval 3600 |
Reconcile even with zero detected changes. |
| manual | usm azsync sync <id> |
Right now. |
And two suppressors:
| Suppressor | Default | Meaning |
|---|---|---|
| rate limit | --min-gap 30 |
Never start within 30s of the last transfer finishing. |
| backoff | — | After a failure: 30s, 60s, 120s … capped at 15m, reset on success. |
A transfer already running never queues a second one; changes that arrive while it runs simply belong to the next batch. If a transfer fails, its batch is merged back so nothing is silently dropped.
# Chatty build output: coalesce hard, tolerate 10 minutes of staleness
usm azsync add ./out https://acct.blob.core.windows.net/artifacts/out \
--quiet-period 15 --batch-files 1000 --max-delay 600
# Precious small files: push almost immediately
usm azsync add ./notes https://acct.blob.core.windows.net/backup/notes \
--quiet-period 2 --min-gap 5
The watcher is only a trigger¶
azcopy sync compares the whole tree on every run, so a missed filesystem
event costs latency, never correctness — the next heartbeat repairs it.
That is what makes the fallbacks safe:
- inotify / FSEvents (via
watchdog) is used when available. - Polling takes over automatically on network mounts, blobfuse
mountpoints, or when the inotify watch limit is hit (
--watch-mode pollforces it,--poll-intervaltunes it). - On very large trees the poller drops its per-file index and falls back to an aggregate signature, marking itself degraded — which just forces the next reconcile.
Excludes are applied in the watcher and translated into azcopy's
--exclude-path / --exclude-pattern / --exclude-regex flags, so .git
churn can't wake the daemon for a transfer that would skip it anyway.
Defaults match usm rsync (.git/, node_modules/, __pycache__/,
*.pyc, …) plus scratch files (*.tmp, *.part, *.swp).
Credentials¶
azcopy fixes the credential when a job starts and cannot rotate it
mid-transfer. azsync therefore refreshes between transfers, and asks for
enough remaining lifetime to cover the next one (adaptively: three times the
last transfer's duration, floor --sas-min-remaining, default 30 min). If a
job dies mid-flight with an expired signature anyway, the token is re-minted
and the job resumed — completed blocks are not re-uploaded.
--auth |
Flag | Source |
|---|---|---|
aad |
— | No SAS at all; azcopy uses your Entra login and refreshes its own token. Prefer this. |
az |
--sas-ttl-hours |
Mint a user-delegation SAS with the Azure CLI (default). |
inline |
— | A sig= already in the URL. Cannot be rotated; you get a warning as it nears expiry. |
env |
--sas-env NAME |
Read an environment variable. |
file |
--sas-file PATH |
Re-read a file on every refresh — works with any external rotator. |
exec |
--sas-command CMD |
Run a command, take stdout. |
http |
--sas-url URL |
GET an endpoint (--sas-header 'K: V', repeatable). |
The four external sources accept a bare token, a full URL, or JSON
({"sas": "...", "expires_at": "..."}). Whatever they return, the expiry is
taken from the token's own se= field — a provider that lies about its
lifetime cannot cause a mid-job failure. Passing any --sas-* flag selects
the matching --auth for you.
# Rotated by an external agent writing to a file
usm azsync add ./data https://acct.blob.core.windows.net/bucket/data \
--sas-file /run/secrets/blob.sas
# Minted on demand by your own service
usm azsync add ./data https://acct.blob.core.windows.net/bucket/data \
--sas-url https://sas.internal/mint?container=bucket \
--sas-header "Authorization: Bearer $TOKEN"
Tokens are cached 0600 and every log line, table and error is redacted
(sig=***). One caveat: azcopy sync has no way to take a SAS other than in
the URL, so it is visible in ps while a transfer runs — use --auth aad
where you can.
Running it¶
usm azsync add ./data https://acct... # define + start watching
usm azsync ls # status, pending changes, SAS clock
usm azsync status data-bucket # detail + recent transfer history
usm azsync sync data-bucket # transfer now
usm azsync dry-run data-bucket # what would move, transfers nothing
usm azsync logs data-bucket -f # follow (--azcopy for azcopy's log)
usm azsync stop|start|restart data-bucket
usm azsync enable data-bucket # systemd user unit / launchd agent
usm azsync rm data-bucket
usm azsync once ./dir <url> runs a single sync without defining anything —
usm cp with excludes and the SAS machinery.
enable writes a systemd user unit (usm-azsync-<id>.service) or a launchd
agent running usm azsync up <id> with Restart=always. On Linux, run
loginctl enable-linger $USER if it should survive logout.
Safety¶
--delete is opt-in and destructive: it removes blobs that no longer exist
locally, on every transfer. Without it nothing is ever deleted at the
destination.
Limits¶
- A large file still being written can be uploaded truncated; the next
transfer repairs it. The quiet period is the mitigation —
azcopyhas no "file must be stable for N seconds" filter. - Comparison is by last-modified time.
--compare-hashswitches to MD5: exact, but it reads every file. stop_reason-style detail is limited by azcopy: it never reports which files it skipped, only counts.- Local → blob only. Pulling the other way is a plain
usm cp.
Flags¶
usm azsync add --help for the full list. The ones worth knowing:
| Flag | Default | Purpose |
|---|---|---|
--auth |
az (or inline) |
Credential source, see above. |
--quiet-period |
5 |
Debounce before transferring. |
--batch-files / --batch-bytes |
200 / 256MiB |
Fire early on big batches. |
--max-delay |
300 |
Staleness cap under constant writes. |
--interval |
3600 |
Reconcile with no changes detected. |
--min-gap |
30 |
Minimum idle time between transfers. |
--watch-mode |
auto |
inotify or poll. |
-e/--exclude |
— | Extra exclude pattern (repeatable). |
--delete |
off | Mirror mode; deletes remote extras. |
--compare-hash |
off | MD5 instead of last-modified time. |
--cap-mbps |
— | Throttle the transfer. |
Prerequisites¶
azcopy— install withusm cp --install, or set$USM_AZCOPY_BIN. Both commands share~/.cache/usm/bin/azcopy.azCLI logged in, for--auth az(the default) and--auth aad.
Shared module¶
The SAS lifecycle, blob URL handling, service units, locking and redaction
live in
scripts/usm_azure.py,
shared verbatim with usm blobmount — so a credential source
added for one command works in the other.
Source¶
scripts/azsync.py
plus the shared usm_azure.py. Layered so each piece is testable on its own:
a pure trigger policy, a watcher protocol with two backends, a SAS provider
protocol with seven, and an azcopy engine — wired together by a single
supervisor.
Test suite at
tests/test_azsync.py
(288 tests, 95% line coverage). The engine and daemon run end to end against a
fake azcopy injected via $USM_AZCOPY_BIN, covering success, partial
failure, credential expiry → resume, network backoff, corrupt state files and
watcher fallback — without touching Azure. The listing is asserted to fit
terminals from 70 to 200 columns and to never print a token.