Skip to content

upload_files: optional resume cache so an interrupted transfer resumes instead of restarting #613

Description

@Caushi

Problem

upload_files can already report each file's resumable tus URL via on_url, and send_to_sink can already consume one via resume_url. But nothing connects them across runs, so an interrupted transfer always restarts from byte 0. From the docstring (operations/workunit/upload.py:185):

Persisting these lets a later run resume via send_to_sink's resume_url; upload_files itself never resumes, since each run mints fresh resources.

For instrument data that matters: a multi-GB Bruker .d bundle that dies at 90% re-sends everything.

Every consumer that wants resume must currently drop below upload_files to send_to_sink, which means giving up the workunit/resource orchestration that is the reason to use upload_files at all. examples/instrument_folder_uploader.py already demonstrates sidecar state files, but as an example — so each caller (that uploader, bfabric-tus-client, BioBeamer) reimplements the same persistence and the same invalidation rules.

Why this belongs in bfabricpy rather than in callers

Deciding whether to resume needs: is there a saved URL, is it same-origin with the current endpoint (_tus_mover._same_origin), does a HEAD still resolve, what offset came back, has tusd expired the upload. All of that lives inside _tus_mover / send_to_sink. A caller implementing it reaches into internals from outside.

There is also a precedent for bfabricpy persisting client-side state across process restarts: oauth/_token_cache.py (TokenCache, ~/.bfabric/tokens/<hash>.json, 0600).

Note the offset needs no new server-side machinery — uploader.get_offset() is a plain tus HEAD against the upload URL (_tus_mover.py:128). Only the URL has to survive between runs. Equally, this must not be pushed into create-resources: that endpoint has no knowledge of the tus layer, and giving it any would put transport state behind a REST API that should stay ignorant of it.

Suggested shape

An opt-in cache path on upload_files, defaulting to today's behaviour:

upload_files(client, params, resume_cache=Path(...) or None)
  • when set, write (file identity) -> upload_url as on_url fires (including for a file whose transfer then fails — that is the case worth keeping)
  • on a later run, look up a saved URL and pass it to send_to_sink(resume_url=...); the HEAD yields the offset, and a stale URL already raises TransferError and can fall back to a fresh upload
  • None (default) keeps current semantics exactly, so no existing caller changes behaviour

Two things worth deciding when implementing:

  • Key: MD5 is the honest identity (it is already computed, and detects a file rewritten in place) rather than path+size+mtime.
  • Invalidation: drop entries whose origin no longer matches tus_endpoint (_same_origin already refuses to send the bearer token cross-origin), plus a TTL matching tusd's configured upload expiry.

Related: #612 — a resume path that reuses the existing pending resource would also make that failure mode self-correcting, since the orphaned resource becomes the checkpoint rather than a blocker.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions