| 1 | /** |
| 2 | * Where files put in pages are kept: behind this small interface, so a |
| 3 | * self-hosted g1t can keep them on disk or in any S3-compatible store. |
| 4 | * The managed service uses R2 (`r2FileStore`). |
| 5 | */ |
| 6 | |
| 7 | export type StoredFile = { body: ReadableStream; content_type: string; bytes: number; etag: string }; |
| 8 | |
| 9 | export interface FileStore { |
| 10 | put(key: string, body: ReadableStream | ArrayBuffer | Uint8Array, contentType: string): Promise<void>; |
| 11 | get(key: string): Promise<StoredFile | null>; |
| 12 | delete(key: string): Promise<void>; |
| 13 | } |
| 14 | |
| 15 | export function r2FileStore(bucket: R2Bucket): FileStore { |
| 16 | return { |
| 17 | async put(key, body, contentType) { |
| 18 | await bucket.put(key, body, { httpMetadata: { contentType } }); |
| 19 | }, |
| 20 | async get(key) { |
| 21 | const object = await bucket.get(key); |
| 22 | if (!object) return null; |
| 23 | return { |
| 24 | body: object.body, |
| 25 | content_type: object.httpMetadata?.contentType ?? "application/octet-stream", |
| 26 | bytes: object.size, |
| 27 | etag: object.httpEtag, |
| 28 | }; |
| 29 | }, |
| 30 | async delete(key) { |
| 31 | await bucket.delete(key); |
| 32 | }, |
| 33 | }; |
| 34 | } |
| 35 | |
| 36 | /** Types a page's file is served as; anything else is served as a download. */ |
| 37 | const INLINE = new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/avif", "video/mp4", "video/webm", "audio/mpeg", "audio/ogg", "audio/wav", "application/pdf"]); |
| 38 | |
| 39 | /** The type a file is kept and served as: images and media as themselves, SVG and everything else as bytes to download (nothing served can run script). */ |
| 40 | export function servedType(contentType: string): string { |
| 41 | const type = String(contentType ?? "").split(";")[0]!.trim().toLowerCase(); |
| 42 | return INLINE.has(type) ? type : "application/octet-stream"; |
| 43 | } |
| 44 | |
| 45 | /** A file name safe to keep and to put in a header. */ |
| 46 | export function safeName(name: string): string { |
| 47 | const clean = String(name ?? "") |
| 48 | .replace(/[\\/:*?"<>|\u0000-\u001f]+/g, "_") |
| 49 | .trim() |
| 50 | .slice(0, 180); |
| 51 | return clean || "file"; |
| 52 | } |