Architecture
Architecture
Moving a directory tree in or out of the DataRobot Files API otherwise means either the
Python SDK’s fsspec layer (per-file, uncompressed, needs a Python runtime) or
hand-rolled curl — neither drops into CI, and both re-send bytes that have not
changed. See
SPEC.md for
the full design; this page is the summary.
Invariants
These are load-bearing. Breaking one is a data-loss or corruption bug, not a style choice.
- Nothing in the container is ever overwritten. Manifests are generation-named, packs
are content-addressed.
overwriteiserror, notreplace. - A pack is uploaded before any manifest referencing it, and deleted only after the last manifest referencing it is gone.
- A manifest is deleted only after a newer one is verified to be the maximum.
- One
fromStagecommit per run wherever possible — it is the atomicity boundary; when split, the manifest commits last. - Everything streams. No
io.ReadAlland nobytes.Bufferon file content, ever — a single file may be larger than RAM. - The
Authorizationheader never reaches a presigned S3 URL (S3 returns 403 when a presigned signature and an auth header both appear). - No persistent local state. Local truth is re-derived by walking and hashing every run; scratch files are disposable and correctness never depends on them surviving.
- Extraction never writes outside the sync root, never writes under the meta prefix, and strips setuid/setgid/sticky unless explicitly asked not to.
What the API actually gives us
Verified live against app.datarobot.com:
- A “container” is a catalog item with
catalogType: files, holding files at relative paths. There are no folder objects — directories are implied by/in the path. GET files/{id}/allFiles/returnsfileChecksum= SHA-256 hex of the raw bytes, confirmed by downloading a file and hashing it.limitdefaults to 100 and caps at 1000, so followingnextis mandatory.- Downloads are presigned S3 URLs, and they honor
Range. - Uploads are multipart, one request per file, at ~0.2s each even at 8-way parallelism. A “stage” batches many of them behind one commit — the whole reason packing, rather than mirroring, is the design.
- DataRobot’s intake will not unpack xz — only zip, tar, tar.gz/tgz, tar.bz2, gz, bz2. Server-side extraction is off the table, so datarobot-sync does all tarring, compressing, and extraction locally.
Decisions this led to
- Opaque packs, real xz. The container holds only
.dr-sync/…objects: immutable manifests plustar.xzpacks, stored as opaque bytes. This is the only route to genuine LZMA2 on the wire. - No local state. All sync state lives in the remote manifest; local disk is not trusted between runs.
- Catalog ID is explicit —
--catalog-idflag orDR_SYNC_CATALOG_ID— with a dedicateddatarobot sync initsubcommand to create the container. - Shipped by goreleaser: per-platform binaries, a ghcr image, and a
drplugin.tar.xzbundling every platform behind a wrapper script. - fsnotify for
watch, notrjeczalik/notify.rjeczalik/notifyneeds CGO for macOS FSEvents, which rules it out under this project’sCGO_ENABLED=0release build. fsnotify is pure Go on every backend, and was already in the module graph as an indirect dependency of viper — using it directly adds no new module to the build. It has no public recursive watch, even in releases newer than the one pinned here (the recursive code path exists but is internal, exercised only by fsnotify’s own tests), sointernal/watcheradds one watch per directory itself.
Two API details the client must get right
fromStageis synchronous and atomic:200with{catalogId, catalogVersionId, numFiles}, noLocation, ~0.35s even for 200 files; on a409nothing from the stage lands. TheLocationpoller is a fallback for large stages, not the hot path.- Presigned URLs come back with an explicit
:443, and SigV4 signed the host without it — Go sends theHostheader exactly as the URL spells it, so the port must be stripped before the request or S3 answersSignatureDoesNotMatch. Nohttptest-based test can catch this, because those always listen on a non-default port — it took a run against the live service.
Non-goals
- Crawling or spidering beyond the target directory tree.
- A mutable pointer object as a lock or CAS token — the API has no compare-and-swap, and a mutable head is exactly where lost updates become corruption.
- Persistence beyond the remote manifest — local state is never trusted between runs.
See Concurrency and garbage collection for what two writers racing each other are actually guaranteed.