Large file storage for AT Protocol: an IPFS server & gateway with XRPC upload & admin. atfs.dev
ipfs atproto xrpc

fix: reject empty-pins Meta at Store.Put and Node.Pin (ATFS-o8ov) master

SweepDeletions treats a stored blob's empty pin list as "a delete crashed between Unpin and removal" and reaps it at boot. That's only sound if the *only* way to reach a persisted empty-pins record is Unpin releasing a last claim — but Store.Put and Node.Pin both took a caller-supplied Meta with no shape guarantee on Pins, so a caller passing an empty claim list could manufacture that same on-disk state for content nobody actually claimed, and lose it silently on the next restart. Guard both write boundaries: - Store.Put now rejects any Meta with an empty Pins list (new ErrEmptyPins sentinel), checked before the reader or disk are touched at all, uniformly for both a fresh blob and a re-Put of content already stored. - Node.Pin rejects an empty meta.Pins up front too, as a definitive failure alongside its existing reference validation. This isn't redundant with Store.Put's guard: Node.Pin's already-stored branch merges through Store.Pin, which quietly no-ops on an empty incoming list rather than erroring, so without this check a pin call against already-stored content with no claim would report false success while recording nothing. Store.Pin itself stays a no-op on empty input — merging nothing into an existing record can't manufacture a new zero-pins state — with a comment on why, since Node.Pin is now the seam that guarantees it never sees empty input in practice. Unpin's empty-list persistence and SweepDeletions' reaping are unchanged; they're exactly the invariant these guards protect. All current Put/Pin callers (xrpc uploadFile, internal/pin's Manager) already supplied non-empty claims, so nothing in the tree was actually exposed — but pinFile stage two had already landed, so the route the bean warned about was live, and several existing tests built fixtures via Put(..., Meta{}) expressly because they didn't care about claims. Updated those to pass a real claim (via each package's existing pinMeta helper), which also makes the fixtures model the real invariant more faithfully.