//! Which bucket an object lives in, and the authority a delete needs. /// Default presigned URL expiration. /// 1 hour balances usability (large uploads over slow connections) against /// security (limiting the window for URL leakage). Overridable per-call. pub(super) const PRESIGN_EXPIRY_SECS: u64 = 3600; /// Cache-Control value for immutable content (builds, audio, covers). /// One year with immutable directive, Cloudflare and browsers cache indefinitely. pub const CACHE_CONTROL_IMMUTABLE: &str = "public, max-age=31536000, immutable"; /// Capability proof required by every `StorageBackend` delete method. /// /// Direct S3 deletion is sealed off from route handlers: the delete methods /// take `&S3DeleteAuthority`, so an accidental `s3.delete_object(key)` from a /// handler does not compile. Route code must instead enqueue through /// `pending_s3_deletions` (e.g. `routes::storage::enqueue_s3_orphan`), whose /// worker applies the `is_s3_key_live` guard before deleting, so a handler /// cannot blind-delete a key a live row still references. /// /// Minting is `pub(crate)` and confined by convention to the durable-deletion /// paths, the scheduler deletion worker + cleanup (`scheduler/cleanup.rs`) and /// the malware-quarantine scan worker (`scanning/worker.rs`). The build-time /// guard test `routes_never_delete_s3_directly` fails if any file under /// `src/routes/` names a delete method or mints an authority, so the seal can't /// silently erode. pub struct S3DeleteAuthority(()); impl S3DeleteAuthority { /// Mint a deletion authority. Restricted to the sanctioned durable-deletion /// paths; see the type docs. Route handlers cannot reach a sanctioned path, /// and the guard test enforces that they don't mint one anyway. pub(crate) fn new() -> Self { S3DeleteAuthority(()) } } /// Which configured S3 backend an object lives in. /// /// The delete *verb* is type-sealed by [`S3DeleteAuthority`]; this seals the /// bucket *noun*. The `pending_s3_deletions` queue stores the bucket as text, and /// the deletion worker dispatches between the main and SyncKit S3 clients on that /// text. This enum is the single source of truth for the `"main"`/`"synckit"` /// spellings so an orphan-enqueue can't silently mis-tag a SyncKit object as /// `main` (where the worker would delete it against the wrong client and leak it /// forever), `enqueue_s3_orphan` requires an `S3Bucket`, not a bare string. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum S3Bucket { Main, Synckit, /// Public, CDN-served bucket (`cdn.makenot.work`). Holds ONLY the /// immutably-public image kinds after promote (covers, gallery, item/project /// images, content insertions); a paid object can never enter it, so its /// blanket public-read policy is safe by construction. Staging is never here ///, unscanned bytes stay in `Main`; the content object lands here only via /// the cross-bucket promote (see `scanning::promote_staging_to_content`). Public, } impl S3Bucket { /// The stored/text spelling for the deletion queue. pub fn as_str(self) -> &'static str { match self { S3Bucket::Main => "main", S3Bucket::Synckit => "synckit", S3Bucket::Public => "public", } } /// Parse a bucket tag read back from the queue. Unknown/legacy values map to /// `Main` (the historical default), so a garbled row is still reaped against /// a backend rather than wedging the queue. pub fn from_db_str(s: &str) -> Self { match s { "synckit" => S3Bucket::Synckit, "public" => S3Bucket::Public, _ => S3Bucket::Main, } } } /// Deletion enqueue pair for a content-image key whose promote state is unknown. /// /// A CDN-image key is a private **staging** key (`staging/...`, in `Main`) until /// [`crate::scanning::promote_staging_to_content`] repoints it to the public /// **content** key (`{user}/c/{sha}.ext`, in `Public`). A given object is in /// EXACTLY one bucket, but a delete/replace path can run in either state, so it /// can't know which. Enqueue the key under BOTH buckets: the reaper deletes from /// the bucket the object is in and no-ops the other (content keys are unique to /// one bucket), and `is_s3_key_live` still guards each bucket against a live /// reference. Only for the four CDN-served image surfaces; gated media /// (audio/video/version/media) is always `Main` and insertions always `Main`. pub fn both_bucket_delete(key: &str) -> [(String, String); 2] { [ (key.to_string(), S3Bucket::Main.as_str().to_string()), (key.to_string(), S3Bucket::Public.as_str().to_string()), ] }