feat: direct-to-R2 Files uploads (client-sealed SPENC2) + chunked video dropzone + multi-file queue
For scale: payload bytes go browser → R2 → browser; the api handles keys, authz and metadata only, so a cheap box can serve thousands of users. - SPENC2 frame (browser-sealed, P-256 ECDH → HKDF wrap of a per-file CEK, 10 MiG AES-GCM segments): storage.ts gains the reader (magic-branched getObject), presignPut/headObject, openDirectHeader; sealForDirect is the reference sealer for cross-implementation tests - Files API: GET /files/direct/config, POST /files/direct/init (presigned PUT), POST /files/direct/complete (HEAD-gated commit), POST /files/:id/direct-download (presigned GET + CEK ticket) - web: lib/direct-storage (WebCrypto seal/open), lib/files-direct (flows), dropzone rewritten with a multi-file sequential queue (per-file status, direct-first with server fallback), file download decrypts in-browser for direct objects (falls back to streaming) - video dropzone wired to the chunked ingest API (>10 MiB → init/chunk/ complete), fixing the Cloudflare ~100 MB body cap for large videos - DIRECT_EC_PRIVATE_KEY (P-256, PKCS8 b64) wired through compose; cross-impl roundtrip tests prove web-seal ↔ server-open byte compatibility
This commit is contained in:
parent
b3c2085436
commit
0a1c3b392b
21 changed files with 1444 additions and 83 deletions
|
|
@ -14,6 +14,7 @@ import { UploadDropzone } from "../../../components/files/upload-dropzone";
|
|||
import { ShareDialog } from "../../../components/files/share-dialog";
|
||||
import { RetentionDialog } from "../../../components/files/retention-dialog";
|
||||
import { downloadGroupFile } from "../../../components/files/files-client";
|
||||
import { downloadFileDirect, isDirectObject, saveBytes } from "../../../lib/files-direct";
|
||||
import { useFilesLang } from "../../../components/files/i18n";
|
||||
import { MIME_CLASS_LABEL } from "../../../components/files/labels";
|
||||
import type {
|
||||
|
|
@ -171,8 +172,23 @@ export default function FilesPage(): React.ReactElement {
|
|||
async function handleDownload(item: GroupFile): Promise<void> {
|
||||
setActionError(null);
|
||||
try {
|
||||
if (isDirectObject(item)) {
|
||||
const bytes = await downloadFileDirect(item.id);
|
||||
saveBytes(bytes, item.name);
|
||||
return;
|
||||
}
|
||||
await downloadGroupFile(item.id, item.name);
|
||||
} catch (err) {
|
||||
// Direct path is best-effort: fall back to streaming via the api once.
|
||||
if (isDirectObject(item)) {
|
||||
try {
|
||||
await downloadGroupFile(item.id, item.name);
|
||||
return;
|
||||
} catch (fallbackErr) {
|
||||
setActionError(errorMessage(fallbackErr));
|
||||
return;
|
||||
}
|
||||
}
|
||||
setActionError(errorMessage(err));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue