| 1 | package xlsx |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "fmt" |
| 7 | |
| 8 | "amadan.net/rastrillo/rastrillo/blobs" |
| 9 | |
| 10 | "amadan.net/oficina/sheets/internal/sheets/store" |
| 11 | ) |
| 12 | |
| 13 | // blobContentType is deliberately opaque. blobs.Sealed encrypts the bytes |
| 14 | // but passes ContentType through as visible metadata (verified directly |
| 15 | // against rastrillo/blobs — see blobs.Sealed's own doc comment), so a |
| 16 | // truthful spreadsheet content type would announce exactly what the |
| 17 | // ciphertext is, in a bucket that is otherwise opaque. Spec §2.3, §11. |
| 18 | const blobContentType = "application/octet-stream" |
| 19 | |
| 20 | // StoreSealed exports h's tab and writes it to underlying through |
| 21 | // blobs.Sealed under key, never letting a plaintext spreadsheet reach |
| 22 | // underlying — spec §0 and §2.3: unsealed on the S3 backend, a generated |
| 23 | // XLSX file is a plaintext spreadsheet in a bucket, the one rule defeated |
| 24 | // through a side door. |
| 25 | // |
| 26 | // This exists because the sealing step belongs next to the bytes it |
| 27 | // seals, not because the rest of the plumbing does: the upload/download |
| 28 | // HTTP surface and the background-job wrapper around Import/Export |
| 29 | // (spec §11: "Import runs as a rastrillo background job") are plan 2 — |
| 30 | // this repo has no job runner, no session-gated routes, and no app-row |
| 31 | // shape for a stored blob's Ref yet. A caller with all of that wires it |
| 32 | // to StoreSealed/LoadSealed rather than to underlying directly. |
| 33 | // |
| 34 | // The blob address returned is the ciphertext's hash (dedup is per key, |
| 35 | // not per plaintext) and Ref.Size is the sealed size — both documented on |
| 36 | // blobs.Sealed itself, restated here because a caller showing either to |
| 37 | // a person (a quota, a progress bar) needs to know. |
| 38 | func StoreSealed(ctx context.Context, underlying blobs.Store, key []byte, h *store.Handle) (blobs.Ref, error) { |
| 39 | var buf bytes.Buffer |
| 40 | if err := Export(h, &buf); err != nil { |
| 41 | return blobs.Ref{}, err |
| 42 | } |
| 43 | ref, err := blobs.Sealed(underlying, key).Put(ctx, &buf, blobContentType) |
| 44 | if err != nil { |
| 45 | return blobs.Ref{}, fmt.Errorf("xlsx: store sealed export: %w", err) |
| 46 | } |
| 47 | return ref, nil |
| 48 | } |
| 49 | |
| 50 | // LoadSealed reads a blob written by StoreSealed (or any XLSX blob sealed |
| 51 | // under key with blobs.Sealed) and imports it into h. |
| 52 | func LoadSealed(ctx context.Context, underlying blobs.Store, key []byte, ref blobs.Ref, h *store.Handle) error { |
| 53 | rc, err := blobs.Sealed(underlying, key).Get(ctx, ref.Hash) |
| 54 | if err != nil { |
| 55 | return fmt.Errorf("xlsx: load sealed import: %w", err) |
| 56 | } |
| 57 | defer rc.Close() |
| 58 | return Import(rc, h) |
| 59 | } |
| 60 | |