Skip to content
File

Blob: src/worker/webdav/put.ts

typescript82 lines
1import { davError, emptyResponse } from "@/worker/dav/http";
2import { maxFileBytes as maxConfiguredFileBytes } from "@/worker/dav/limits";
3import type { SubjectRow } from "@/worker/db/d1/schema";
4import { blobGcMessage } from "@/worker/queues/blob-gc";
5import { BodyTooLargeError, putFileBlob } from "@/worker/r2/blobs";
6import { enqueueBlobGc } from "@/worker/r2/gc";
7import type { AppContext } from "@/worker/types";
8import { fileDavObject, resultError, type NormalizedFilesRequestPath } from "@/worker/webdav/object";
9import { parseContentLength, readFileDavWriteRequest } from "@/worker/webdav/request";
10 
11export async function handlePut(c: AppContext, subject: SubjectRow, path: NormalizedFilesRequestPath) {
12 const writeRequest = readFileDavWriteRequest(c.req.raw);
13 if (writeRequest instanceof Response) return writeRequest;
14 const contentType = c.req.header("content-type") ?? "application/octet-stream";
15 const contentLength = parseContentLength(c.req.raw);
16 const maxFileBytes = maxConfiguredFileBytes(c.env);
17 const object = fileDavObject(c.env, subject.storageId);
18 const begin = await object.beginWrite({
19 subjectId: subject.id,
20 storageId: subject.storageId,
21 path: path.path,
22 contentLength,
23 contentType,
24 contentLanguage: c.req.header("content-language") ?? null,
25 ...writeRequest,
26 maxFileBytes,
27 });
28 if (!begin.ok) return resultError(begin);
29 
30 let r2Result: { size: number; etag: string | null };
31 try {
32 r2Result = await putFileBlob(c.env.FILE_BLOBS, {
33 key: begin.blobKey,
34 body: c.req.raw.body,
35 contentType,
36 contentLength,
37 maxBytes: maxFileBytes,
38 });
39 } catch (cause) {
40 await object.abortWrite({ uploadId: begin.uploadId });
41 if (cause instanceof BodyTooLargeError) return davError(413, cause.message);
42 throw cause;
43 }
44 
45 const commit = await object.commitWrite({
46 subjectId: subject.id,
47 storageId: subject.storageId,
48 uploadId: begin.uploadId,
49 size: r2Result.size,
50 r2Etag: r2Result.etag,
51 contentType,
52 contentLanguage: c.req.header("content-language") ?? null,
53 nowMs: Date.now(),
54 });
55 if (!commit.ok) {
56 await object.abortWrite({ uploadId: begin.uploadId });
57 await enqueueBlobGc(c.env, [
58 blobGcMessage({
59 subjectId: subject.id,
60 storageId: subject.storageId,
61 blobId: begin.blobId,
62 blobKey: begin.blobKey,
63 notBeforeMs: Date.now(),
64 }),
65 ]);
66 return resultError(commit);
67 }
68 await enqueueBlobGc(
69 c.env,
70 commit.blobGc.map((blob) =>
71 blobGcMessage({
72 subjectId: subject.id,
73 storageId: subject.storageId,
74 blobId: blob.blobId,
75 blobKey: blob.blobKey,
76 notBeforeMs: Date.now(),
77 }),
78 ),
79 );
80 return emptyResponse(commit.created ? 201 : 204, { ETag: commit.node.etag ?? "" });
81}