File
Blob: src/worker/webdav/get-head.ts
| 1 | import { davError } from "@/worker/dav/http"; |
| 2 | import { maxDavDepthInfinityNodes } from "@/worker/dav/limits"; |
| 3 | import { parentHref } from "@/worker/dav/paths"; |
| 4 | import { davHtmlListingResponse, type DavListingEntry } from "@/worker/dav/html-listing"; |
| 5 | import type { SubjectRow } from "@/worker/db/d1/schema"; |
| 6 | import type { FileDavNodeView } from "@/worker/objects/file-dav-object"; |
| 7 | import { getFileBlob } from "@/worker/r2/blobs"; |
| 8 | import type { AppContext } from "@/worker/types"; |
| 9 | import { fileDavObject, resultError, type NormalizedFilesRequestPath } from "@/worker/webdav/object"; |
| 10 | import { httpDate } from "@/worker/webdav/props"; |
| 11 | |
| 12 | function fileListingName(node: { name: string }, href: string): string { |
| 13 | if (href === "/files/") return "Files"; |
| 14 | return node.name || href; |
| 15 | } |
| 16 | |
| 17 | function fileListingEntries(nodes: FileDavNodeView[]) { |
| 18 | return nodes.slice(1).map((entry): DavListingEntry => { |
| 19 | const isCollection = entry.node.kind === "collection"; |
| 20 | return { |
| 21 | href: entry.href, |
| 22 | name: fileListingName(entry.node, entry.href), |
| 23 | kind: isCollection ? "collection" : "file", |
| 24 | size: isCollection ? null : entry.node.size, |
| 25 | modifiedAtMs: entry.node.modifiedAtMs, |
| 26 | }; |
| 27 | }); |
| 28 | } |
| 29 | |
| 30 | async function handleCollectionGetHead(c: AppContext, subject: SubjectRow, path: NormalizedFilesRequestPath) { |
| 31 | const result = await fileDavObject(c.env, subject.storageId).propfind({ |
| 32 | subjectId: subject.id, |
| 33 | storageId: subject.storageId, |
| 34 | path: path.path, |
| 35 | depth: "1", |
| 36 | maxNodes: maxDavDepthInfinityNodes(c.env), |
| 37 | nowMs: Date.now(), |
| 38 | }); |
| 39 | if (!result.ok) return resultError(result); |
| 40 | const target = result.nodes[0]; |
| 41 | if (!target || target.node.kind !== "collection") return davError(405, "Resource is not a collection"); |
| 42 | return await davHtmlListingResponse( |
| 43 | c, |
| 44 | { |
| 45 | title: fileListingName(target.node, target.href), |
| 46 | href: target.href, |
| 47 | parentHref: path.path.segments.length === 0 ? "/" : parentHref(path.path), |
| 48 | entries: fileListingEntries(result.nodes), |
| 49 | }, |
| 50 | c.req.method, |
| 51 | ); |
| 52 | } |
| 53 | |
| 54 | export async function handleGetHead(c: AppContext, subject: SubjectRow, path: NormalizedFilesRequestPath) { |
| 55 | const result = await fileDavObject(c.env, subject.storageId).resolveFile({ |
| 56 | subjectId: subject.id, |
| 57 | storageId: subject.storageId, |
| 58 | path: path.path, |
| 59 | ifMatch: c.req.header("if-match") ?? null, |
| 60 | ifNoneMatch: c.req.header("if-none-match") ?? null, |
| 61 | nowMs: Date.now(), |
| 62 | }); |
| 63 | if (!result.ok && result.status === 405 && result.code === "method_not_allowed") { |
| 64 | return await handleCollectionGetHead(c, subject, path); |
| 65 | } |
| 66 | if (!result.ok) return resultError(result); |
| 67 | const object = result.blob ? await getFileBlob(c.env.FILE_BLOBS, result.blob.blobKey) : null; |
| 68 | if (result.blob && !object) return davError(404, "Blob not found"); |
| 69 | const headers = new Headers({ |
| 70 | "Content-Length": String(result.node.size), |
| 71 | "Content-Type": result.node.contentType ?? object?.httpMetadata?.contentType ?? "application/octet-stream", |
| 72 | "Last-Modified": httpDate(result.node.modifiedAtMs), |
| 73 | }); |
| 74 | if (result.node.etag) headers.set("ETag", result.node.etag); |
| 75 | if (c.req.method === "HEAD") return new Response(null, { status: 200, headers }); |
| 76 | return new Response(object?.body ?? null, { status: 200, headers }); |
| 77 | } |