Skip to main content

max / makenotwork

4.6 KB · 99 lines History Blame Raw
1 //! Which bucket an object lives in, and the authority a delete needs.
2
3 /// Default presigned URL expiration.
4 /// 1 hour balances usability (large uploads over slow connections) against
5 /// security (limiting the window for URL leakage). Overridable per-call.
6 pub(super) const PRESIGN_EXPIRY_SECS: u64 = 3600;
7
8 /// Cache-Control value for immutable content (builds, audio, covers).
9 /// One year with immutable directive, Cloudflare and browsers cache indefinitely.
10 pub const CACHE_CONTROL_IMMUTABLE: &str = "public, max-age=31536000, immutable";
11
12 /// Capability proof required by every `StorageBackend` delete method.
13 ///
14 /// Direct S3 deletion is sealed off from route handlers: the delete methods
15 /// take `&S3DeleteAuthority`, so an accidental `s3.delete_object(key)` from a
16 /// handler does not compile. Route code must instead enqueue through
17 /// `pending_s3_deletions` (e.g. `routes::storage::enqueue_s3_orphan`), whose
18 /// worker applies the `is_s3_key_live` guard before deleting, so a handler
19 /// cannot blind-delete a key a live row still references.
20 ///
21 /// Minting is `pub(crate)` and confined by convention to the durable-deletion
22 /// paths, the scheduler deletion worker + cleanup (`scheduler/cleanup.rs`) and
23 /// the malware-quarantine scan worker (`scanning/worker.rs`). The build-time
24 /// guard test `routes_never_delete_s3_directly` fails if any file under
25 /// `src/routes/` names a delete method or mints an authority, so the seal can't
26 /// silently erode.
27 pub struct S3DeleteAuthority(());
28
29 impl S3DeleteAuthority {
30 /// Mint a deletion authority. Restricted to the sanctioned durable-deletion
31 /// paths; see the type docs. Route handlers cannot reach a sanctioned path,
32 /// and the guard test enforces that they don't mint one anyway.
33 pub(crate) fn new() -> Self {
34 S3DeleteAuthority(())
35 }
36 }
37
38 /// Which configured S3 backend an object lives in.
39 ///
40 /// The delete *verb* is type-sealed by [`S3DeleteAuthority`]; this seals the
41 /// bucket *noun*. The `pending_s3_deletions` queue stores the bucket as text, and
42 /// the deletion worker dispatches between the main and SyncKit S3 clients on that
43 /// text. This enum is the single source of truth for the `"main"`/`"synckit"`
44 /// spellings so an orphan-enqueue can't silently mis-tag a SyncKit object as
45 /// `main` (where the worker would delete it against the wrong client and leak it
46 /// forever), `enqueue_s3_orphan` requires an `S3Bucket`, not a bare string.
47 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
48 pub enum S3Bucket {
49 Main,
50 Synckit,
51 /// Public, CDN-served bucket (`cdn.makenot.work`). Holds ONLY the
52 /// immutably-public image kinds after promote (covers, gallery, item/project
53 /// images, content insertions); a paid object can never enter it, so its
54 /// blanket public-read policy is safe by construction. Staging is never here
55 ///, unscanned bytes stay in `Main`; the content object lands here only via
56 /// the cross-bucket promote (see `scanning::promote_staging_to_content`).
57 Public,
58 }
59
60 impl S3Bucket {
61 /// The stored/text spelling for the deletion queue.
62 pub fn as_str(self) -> &'static str {
63 match self {
64 S3Bucket::Main => "main",
65 S3Bucket::Synckit => "synckit",
66 S3Bucket::Public => "public",
67 }
68 }
69
70 /// Parse a bucket tag read back from the queue. Unknown/legacy values map to
71 /// `Main` (the historical default), so a garbled row is still reaped against
72 /// a backend rather than wedging the queue.
73 pub fn from_db_str(s: &str) -> Self {
74 match s {
75 "synckit" => S3Bucket::Synckit,
76 "public" => S3Bucket::Public,
77 _ => S3Bucket::Main,
78 }
79 }
80 }
81
82 /// Deletion enqueue pair for a content-image key whose promote state is unknown.
83 ///
84 /// A CDN-image key is a private **staging** key (`staging/...`, in `Main`) until
85 /// [`crate::scanning::promote_staging_to_content`] repoints it to the public
86 /// **content** key (`{user}/c/{sha}.ext`, in `Public`). A given object is in
87 /// EXACTLY one bucket, but a delete/replace path can run in either state, so it
88 /// can't know which. Enqueue the key under BOTH buckets: the reaper deletes from
89 /// the bucket the object is in and no-ops the other (content keys are unique to
90 /// one bucket), and `is_s3_key_live` still guards each bucket against a live
91 /// reference. Only for the four CDN-served image surfaces; gated media
92 /// (audio/video/version/media) is always `Main` and insertions always `Main`.
93 pub fn both_bucket_delete(key: &str) -> [(String, String); 2] {
94 [
95 (key.to_string(), S3Bucket::Main.as_str().to_string()),
96 (key.to_string(), S3Bucket::Public.as_str().to_string()),
97 ]
98 }
99