Lolly
Spustit aplikaci

Resumable uploads

Poznámka: Lolly Work

This guide covers the optional organisation service. Configuration, console and API paths refer to your Work instance. The packaged console keeps its release-matched copy of this guide.

Decision record, 2026-10-17. It chooses the protocol for every upload into Lolly Work that may be larger than one request, and defines the shared primitive that project files, catalog submission, session staging and automation share. The routes are in the API reference and the limits in configuration.

Context

Work takes files in from four places:

Project files already used a parts protocol: the begin declares the size, the whole file's sha256 and one sha256 per 1 MiB part; parts arrive in any order; finalize checks everything. Two things were missing. A client that lost its place could not ask which parts had arrived, so an interrupted upload started over. And migration 0041 capped a project file at 256 MiB. Catalog submission and automation had no way to send a file larger than one request at all.

The constraints that decide the protocol:

Options

A tus 1.0 subset

tus creates an upload with POST (Upload-Length, Upload-Metadata), reports progress with HEAD (Upload-Offset), and appends with PATCH at that offset. Extensions add expiry (Upload-Expires), per-request checksums (Upload-Checksum), termination (DELETE) and parallel partial uploads joined by concatenation.

Adopting it here would mean:

The parts protocol with a resume report

Keep begin, parts and finalize, and add a report of which parts are stored. Take from tus the ideas that apply: a declared length and metadata at the begin, an offset to carry on from, an expiry the client can read, and termination by DELETE.

Decision

Work keeps the parts protocol and adds the resume report. It does not adopt tus.

The resume report answers GET .../parts with the parts still missing, the receivedParts and receivedBytes, and offset: the bytes in the run of stored parts from the start, which is what tus calls Upload-Offset. It also gives expiresAt, the equivalent of Upload-Expires. Asking does not keep an upload alive; only an accepted part does.

One primitive, four scopes

server/src/uploads/parts.ts is the shared primitive: the manifest rules, storing a verified part, the resume report, the running digest that spares finalize a second read, the finalize check and the verified read. Each scope keeps its own record, authorization, budget and lifetime for the shared upload process.

ScopeRecordWho may beginWho may send, resume and finishLifetimeUsed by
Project file, and so a session's staged imagesproject_filesan editor of the project with session.createthe uploaderuntil deletedthe project's file list and downloads, sessions saved in the project, renders through their read ticket, catalog submission by projectId and fileId
Catalog submission originaluploads, scope catalog-submissiona person or a service token with catalog.submitthe ownerpolicy.uploads.readyTtlHours after it finishedPOST /api/v1/catalog/submit?uploadId=…&checksum=…, which removes it once the submission is stored
Automation inputuploads, scope automation-inputa person or a service token with tool.usethe ownerpolicy.uploads.readyTtlHours after it finishedupload: { id, checksum } in /api/v1/inspect and /api/v1/optimize, as often as the owner likes
File referenceuploads, scope filea person or a service token with tool.usethe ownerpolicy.uploads.fileTtlHours after it finishedfile: { id, checksum } in /api/v1/inspect and /api/v1/optimize, and reading it back from /api/v1/files/:id/content, by the owner while they hold tool.use

A scoped upload is named by reference only, and only by its owner. A reference carries the checksum too, so an id cannot stand in for other bytes. Any reference its owner cannot use (another owner's, another scope's, an unfinished or expired one) gets the same 404, so ids reveal nothing. Nobody downloads a catalog submission original or an automation input; a file reference is the one scope its owner reads back.

A session-scoped area that expires with its session, if one is wanted, would be one more scope on the uploads table over the same primitive.

Limits, budgets and expiry

Resuming from a client

  1. Before sending a part, keep the upload id with the file's digest.
  2. After an interruption, ask GET .../parts.
  3. Send each part in missing, then finalize. A finalize that answers 409 UPLOAD_INCOMPLETE means a part is still missing: ask again.
  4. On 404 or 410 the upload is gone or expired: begin again.

lw upload <file> --project <id> or --scope catalog-submission|automation-input does this. It reads the file one part at a time and keeps each unfinished upload's id under ~/.config/lolly-work/uploads/; running the same command again carries on.

Consequences

Verification