Repository navigation
A hosted stack's owner can copy it out over the wire #427
Description
Problem
copyStack() (#426) is the one primitive for export, import and moving between storage engines. It reads its source only through StackAdapter methods, so an APIAdapter opened as the owner should be a source like any other. Two things stop that (portability.md § Copying a stack):
- Blobs can't be enumerated. A source needs
listBlobs(), and the wire has no endpoint that lists blobs, soAPIAdapterdoesn't implement it.copyStack()refuses it withInvalidAdapterErrorbefore anything is read. - Nothing holds the source still. The spec says the source is not written during a copy and that nothing enforces it, because a local source's only writer is the process copying it. A host keeps taking writes from members and apps, so a copy over the wire yields a target that matches no moment of the source.
Without both, a group on a host can't leave without the host's help.
Decision
No export endpoint and no second format. The wire carries what copyStack() needs from a source, and the copy is the same call it is locally:
const host = await APIAdapter.open({ url, credential, ownerEntityId });
const exported = await copyStack(host, portable('./my-stack.db'));- A blob listing,
GET /blobsor similar, answeringBlobInfofor every blob the store holds. It's served to the owner acting alone (isOwnerActingAlone()), the gateincludeUnlisted, purge andPOST /typesalready use, and refused with403otherwise, delegation included. A blob listing reveals files no Record the requester reaches names. APIAdapter.listBlobs(), present when discovery advertises the endpoint.- A consistent copy, by whatever mechanism the questions below settle. It's stated in
portability.mdas a rule every source meets, not an exception for the wire.
The rest of a copy already travels: an owner reads every Record with includeDeleted and includeUnlisted, every version and journal entry, every Type, and every blob by fileId. #471 tests that path against a real server.
Questions
- Consistency. Options: the server pauses writes for the owner's copy; the server serves reads as of one point in time (a snapshot token on each request); or the copy reads the change feed from where it started and refuses if anything landed. Which one keeps the rule the same for local and remote sources?
- Paging.
listBlobs()returns every blob in one array. Does the wire page it, and if so does the adapter method page too, so local and remote stay one contract? - Garbage collection. Once
APIAdapterhaslistBlobs(),Stack's collection over the wire can find bare-bytes orphans. Is that wanted, or does the owner-alone gate already make it safe?
Actions
- Spec: the endpoint in
wire-format.md, its owner-alone gate, and its discovery key; the consistency rule inportability.md § Copying a stack; drop the sentence saying a stack behind anAPIAdaptercan't be copied out. -
@haverstack/wire-types: the response type and a discovery reader, likesupportsInstallRequests(). - Conformance fixtures: an owner listing, and a refused non-owner and delegated request.
-
APIAdapter.listBlobs(), and a mocked-fetchtest thatcopyStack()accepts it as a source. - Changesets for
@haverstack/core(ifportability.md's rule changes the copy),@haverstack/wire-types,@haverstack/conformance-fixturesand@haverstack/adapter-api.
Order
Unblocked by #426. Land after #472 (it may change the versions and journal endpoints a copy pages) and #473 (it changes discovery). Unblocks #471.
Activity
[-]An export endpoint only the owner can call[/-][+]A hosted stack's owner can copy it out over the wire[/+]