Prototype #
Status: Working (2026-10-15). This is the
Phase P prototype:
the filesync CLI and the filesync-core library, in crates/.
What it does #
- Encrypted hub on any S3-compatible store (tested against MinIO), or on a local directory.
- Roots, mounted at any path, with a different path on each device.
status,sync(push now), andpull(pull now), plusrun --interval Nfor unattended polling.- Three-way sync: changes flow both ways, conflicting edits keep both versions as a conflict copy, and an edit beats a delete.
- Receive-only mounts (
mount --receive-only). - Portable-name checks (Windows-invalid names and case/normalization collisions are skipped, with a warning), symlinks skipped, a per-file size limit (100 MiB by default).
- Version history (
history) and restore of a whole root or one path from any snapshot (restore). - Refuses to sync a root whose folder is missing, or to propagate “everything deleted” without
--allow-mass-delete.
What it deliberately does not do (yet) #
| Shortcut | Final design |
|---|---|
| One shared hub key, copied to each device by hand | PKI device identity, enrollment, revocation (SEC-7, SEC-9..12) |
| Whole-file blobs, no chunking | Content-defined chunking (PERF-3) |
| Blob sizes visible to the hub | Padding or chunking to hide sizes (SEC-2) |
| Every snapshot lists every path | Incremental snapshots or a compacted index |
| No garbage collection or retention | VER-2, VER-6, HUB-7 |
Polling only (run --interval) | Filesystem watching plus rescans (LNX-7) |
| One static S3 credential, with delete rights | Per-device, least-privilege credentials (HUB-6, HUB-7) |
Build and test #
cargo build --release # target/release/filesync
cargo test # unit tests, plus two-device tests on a directory hub
Try it with two “devices” on one machine #
Each device is a config directory plus a state directory. A directory hub needs no credentials:
F=target/release/filesync
export FILESYNC_CONFIG_DIR=/tmp/fs/a/cfg FILESYNC_STATE_DIR=/tmp/fs/a/state
$F init --device laptop --hub-dir /tmp/fs/hub
$F root create Finance
$F mount Finance ~/Finance
$F sync
export FILESYNC_CONFIG_DIR=/tmp/fs/b/cfg FILESYNC_STATE_DIR=/tmp/fs/b/state
$F init --device desktop --hub-dir /tmp/fs/hub --join /tmp/fs/a/cfg/hub.key
$F mount Finance /tmp/fs/b/Money
$F sync
Using Deevnet’s MinIO #
The tenant state store, https://tfstate.mobile.deevnet.net, works as a hub: the client trusts the
Deevnet Root CA through the system trust store, and requests reach MinIO and are authenticated.
What’s needed is a scoped user for a dedicated bucket. The prototype should never use MinIO’s
root credentials.
Where: the tenant state store runs as the minio podman container on dv02prv001v01
(the tenant_state group in the inventory), deployed by the minio role in
ansible-collection-deevnet.mgmt. MinIO’s client mc ships inside the container, and the role
has already registered an alias, local, logged in as root. You never handle the root
credentials: run mc through podman exec, and they stay inside the container.
On dv02prv001v01:
# 0. Check the alias exists (it is lost if the container is recreated; step 0b restores it
# from the container's own environment, without the password ever being shown)
sudo podman exec minio mc alias list local
sudo podman exec minio sh -c 'mc alias set local http://127.0.0.1:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"' # 0b, only if needed
# 1. The bucket
sudo podman exec minio mc mb local/filesync-proto
# 2. A policy allowing that bucket only
sudo podman exec -i minio sh -c 'cat > /tmp/filesync-proto.json' <<'POLICY'
{
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetBucketLocation"],
"Resource": ["arn:aws:s3:::filesync-proto"] },
{ "Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": ["arn:aws:s3:::filesync-proto/*"] }
]
}
POLICY
sudo podman exec minio mc admin policy create local filesync-proto /tmp/filesync-proto.json
# 3. A user with that policy. Generate the secret here; it is this user's only copy.
SECRET=$(openssl rand -hex 20); echo "$SECRET"
sudo podman exec minio mc admin user add local filesync-proto "$SECRET"
sudo podman exec minio mc admin policy attach local filesync-proto --user filesync-proto
The access key is filesync-proto, and the secret is the value printed in step 3. To keep a
record of it, add it to the vault (for example as vault_minio_filesync_proto_secret).
This is a hand-made change to a substrate service, outside Ansible. That’s fine for a prototype,
but if the bucket outlives Phase P, it should move into the minio role (or the Deevnet API’s
state-store provisioning) so the code stays the state.
Then, on each device, put the credentials where only that user can read them:
mkdir -p ~/.config/deevnet-filesync && chmod 700 ~/.config/deevnet-filesync
cat > ~/.config/deevnet-filesync/credentials.toml <<'CREDS'
access_key_id = "filesync-proto"
secret_access_key = "<the secret>"
CREDS
chmod 600 ~/.config/deevnet-filesync/credentials.toml
filesync init --device <name> --s3-endpoint https://tfstate.mobile.deevnet.net --bucket filesync-proto
Environment variables FILESYNC_S3_ACCESS_KEY_ID and FILESYNC_S3_SECRET_ACCESS_KEY override the
file.
Findings for Phase 0 #
What building it taught, to carry into the design documents:
- Conditional writes are enough to coordinate. “Create snapshot N+1 only if absent” makes
each change to a root single-writer, with no server-side code (HUB-4, HUB-5). MinIO honors
If-None-Match: *on PUT. - Snapshots double as version history. Every snapshot is a complete, restorable state of a root, so point-in-time restore (VER-5) comes almost for free. The cost is that the snapshot grows with the root, and is rewritten on every change. Phase 0 should design incremental snapshots and decide how retention prunes them.
start-afterlisting keeps polling cheap. A device only lists snapshots newer than the one it has, so an idle poll is one small LIST request (PERF-2).- A hand-written SigV4 signer is small. It’s about 150 lines, with no AWS SDK, which supports the independence principle (IND-1). It works against MinIO; it still needs testing against AWS S3 itself.
- Keyed content identifiers stop the hub from confirming whether it holds a known file, and cost nothing extra.
- Mass-deletion guard. An empty root folder (unmounted disk, denied permission) looks exactly like “the user deleted everything”. The guard is necessary; it should become a requirement.
- Deletes need care against races. The client deletes a local file only if it still matches what the scan saw; otherwise the edit wins on the next run.
- Blob uploads always send the body, even when the blob already exists. With whole-file blobs this is wasteful on large files; chunking (PERF-3) or a cheap existence check should fix it.
- MinIO’s community container images are gone from public registries, so pinned tags no longer pull. Local S3 testing needs either a locally held image or another S3-compatible server (see Build Tooling).