From a14c0d6bd5ab6a48593d26d6349b872a820ee4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ai=E5=90=9B?= Date: Wed, 11 Sep 2024 17:00:43 +0000 Subject: [PATCH] add --- src/handlers/requestHandler.ts | 4 +- src/handlers/webdavHandler.ts | 147 +++++++++++++++------------------ src/types.ts | 15 ++-- src/utils/auth.ts | 7 -- src/utils/cors.ts | 4 +- src/utils/webdavUtils.ts | 72 ++++++---------- 6 files changed, 98 insertions(+), 151 deletions(-) diff --git a/src/handlers/requestHandler.ts b/src/handlers/requestHandler.ts index af41a92..a59586c 100644 --- a/src/handlers/requestHandler.ts +++ b/src/handlers/requestHandler.ts @@ -2,9 +2,9 @@ import { Env } from '../types'; import { handleWebDAV } from './webdavHandler'; import { authenticate } from '../utils/auth'; import { setCORSHeaders } from '../utils/cors'; -import { logger } from '../utils/logger'; +import { logger/logger'; -export async function handleRequest(request: Request, env: Env, ctx: ExecutionContext): Promise { +export async function handleRequest(request: Request, env: ExecutionContext): Promise { try { if (request.method !== "OPTIONS" && !authenticate(request, env)) { return new Response("Unauthorized", { diff --git a/src/handlers/webdavHandler.ts b/src/handlers/webdavHandler.ts index 4567d75..d7ec36d 100644 --- a/src/handlers/webdavHandler.ts +++ b/src/handlers/webdavHandler.ts @@ -1,4 +1,4 @@ -import { listAll, fromR2Object, make, generatePropfindResponse } from '../utils/webdavUtils'; +import { listAll, fromR2Object, make_resource_path, generatePropfindResponse } from '../utils/webdavUtils'; import { logger } from '../utils/logger'; import { WebDAVProps } from '../types'; @@ -89,81 +89,74 @@ async function handleGet(request: Request, bucket: R2Bucket): Promise }); } -async function handlePut(request: Request, bucket: R2Bucket): Promise { +async function handlePut( bucket: R2Bucket): Promise { const resource_path = make_resource_path(request); + const ifMatch = request.headers.get("If-Match"); + const ifNoneMatch = request.headers.get("If-None-Match"); - try { - const body = await request.arrayBuffer(); - await bucket.put(resource_path, body, { - httpMetadata: { - contentType: request.headers.get("Content-Type") || "application/oc - }, - }); - return new Response("Created", { status: 201 }); - } catch (error) { - logger.error("Error in PUT:", error); - return new Response("Internal Server Error", { status: 500 }); + if (ifMatch || ifNoneMatch) { + const existingObject = await bucket.head(resource_path); + if (ifMatch && existingObject?.etag !== ifMatch) { + return new Response("Precondition Failed", { status: 412 }); + } + if (ifNoneMatch === "*" && existingObject) { + return new Response("Precondition Failed", { status: 412 }); + } } + + const body = await request.arrayBuffer(); + await bucket.put(resource_path, body, { + httpMetadata: { + contentType: request.headers.get("Content-Type") || "application/octet-stream", + }, + }); + + return new Response(null, { status: 201 }); } async function handleDelete(request: Request, bucket: R2Bucket): Promise { const resource_path = make_resource_path(request); - - try { - await bucket.delete(resource_path); - return new Response("No Content", { status: 204 }); - } catch (error) { - logger.error("Error in DELETE:", error); - return new Response("Internal Server Error", { status: 500 }); - } + await bucket.delete(resource_path); + return new Response(null, { status: 204 }); } -async function handleM: Request, bucket: R2Bucket): Promise { +async function handleMkcol(request: Request, bucket: R2Bucket): Promise { const resource_path = make_resource_path(request); if (resource_path === "") { return new Response("Method Not Allowed", { status: 405 }); } - try { - await bucket.put(resource_path + "/.keep", new Uint8Array(), { - customMetadata: { resourcetype: "collection" } - }); - return new Response("Created", { status: 201 }); - } catch (error) { - logger.error("Error in MKCOL:", error); - return new Response("Internal Server Error", { status: 500 }); - } + await bucket.put(resource_path + "/.keep", new Uint8Array(), { + customMetadata: { resourcetype: "collection" } + }); + + return new Response(null, { status: 201 }); } -async function handlePropfind(request: Request, bucket: R2Bucket, bucketName: string): Promise { +asyncfind(request: Request, bucket: R2Bucket, bucketName: string): Promise { const resource_path = make_resource_path(request); const depth = request.headers.get("Depth") || "infinity"; - try { - const props: WebDAVProps[] = []; - if (depth !== "0") { - for await (const object of listAll(bucket, resource_path)) { - props.push(fromR2Object(object, bucketName)); - } + const props: WebDAVProps[] = []; + if (depth !== "0") { + for await (const object of listAll(bucket, resource_path)) { + props.push(fromR2Object(object, resource_path)); + } + } else { + const object = await bucket.head(resource_path); + if (object) { + props.push(fromR2Object(object, resource_path)); } else { - const object = await bucket.head(resource_path); - if (object) { - props.push(fromR2Object(object, bucketName)); - } else { - return new Response("Not Found", { status: 404 }); - } + return new Response("Not Found", { status: 404 }); } - - const xml = generatePropfindResponse(bucketName, resource_path, props); - return new Response(xml, { - status: 207, - headers: { "Content-Type": "application/xml; charset=utf-8" } - }); - } catch (error) { - logger.error("Error in PROPFIND:", error); - return new Response("Internal Server Error", { status: 500 }); } + + const xml = generatePropfindResponse(bucketName, resource_path, props); + return new Response(xml, { + status: 207, + headers: { "Content-Type": "application/xml; charset=utf-8" } + }); } async function handleCopy(request: Request, bucket: R2Bucket): Promise { @@ -175,22 +168,17 @@ async function handleCopy(request: Request, bucket: R2Bucket): Promise const destinationUrl = new URL(destinationHeader); const destinationPath = make_resource_path(new Request(destinationUrl)); - try { - const sourceObject = await bucket.get(sourcePath); - if (!sourceObject) { - return new Response("Not Found", { status: 404 }); - } + const sourceObject = await bucket.get(sourcePath); + if (!sourceObject) { + return new Response("Not Found", { status: 404 }); + } - await bucket.put(destinationPath, sourceObject.body, { - httpMetadata: sourceObject.httpMetadata, - customMetadata: sourceObject.customMetadata - }); + await bucket.put(destinationPath, sourceObject.body, { + httpMetadata: sourceObject.httpMetadata, + customMetadata: sourceObject.customMetadata + }); - return new Response("Created", { status: 201 }); - } catch (error) { - logger.error("Error in COPY:", error); - return new Response("Internal Server Error", { status: 500 }); - } + return new Response(null, { status: 201 }); } async function handleMove(request: Request, bucket: R2Bucket): Promise { @@ -202,22 +190,17 @@ async function handleMove(request: Request, bucket: R2Bucket): Promise const destinationUrl = new URL(destinationHeader); const destinationPath = make_resource_path(new Request(destinationUrl)); - try { - const sourceObject = await bucket.get(sourcePath); - if (!sourceObject) { - return new Response("Not Found", { status: 404 }); - } + const sourceObject = await bucket.get(sourcePath); + if (!sourceObject) { + return new Response("Not Found", { status: 404 }); + } - await bucket.put(destinationPath, sourceObject.body, { - httpMetadata: sourceObject.httpMetadata, - customMetadata: sourceObject.customMetadata - }); + await bucket.put(destinationPath, sourceObject.body, { + httpMetadata: sourceObject.httpMetadata, + customMetadata: sourceObject.customMetadata + }); - await bucket.delete(sourcePath); + await bucket.delete(sourcePath); - return new Response("No Content", { status: 204 }); - } catch (error) { - logger.error("Error in MOVE:", error); - Internal Server Error", { status: 500 }); - } + return new Response(null, { status: 204 }); } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 85f7af7..baf72dc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,21 +2,16 @@ export interface Env { BUCKET: R2Bucket; USERNAME: string; PASSWORD: string; - BUCKET_NAME: string; // 新增的环境变量 -} - -export interface CacheableResponse { - response: Response; - expiry: number; + BUCKET_NAME: string; } export interface WebDAVProps { creationdate: string; - displayname: string | undefined; - getcontentlanguage: string | undefined; + displayname: string; getcontentlength: string; - getcontenttype: string | undefined; - getetag: string | undefined; + getcontenttype: string; + getetag: string; getlastmodified: string; resourcetype: string; + iscollection: boolean; } \ No newline at end of file diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 32343f1..3c9bb09 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,10 +1,3 @@ -// import { Env } from '../types'; - -// export function authenticate(request: Request, env: Env): boolean { -// const authHeader = request.headers.get("Authorization"); -// const expectedAuth = `Basic ${btoa(`${env.USERNAME}:${env.PASSWORD}`)}`; -// return authHeader === expectedAuth; -// } import { Env } from '../types'; export function authenticate(request: Request, env: Env): boolean { diff --git a/src/utils/cors.ts b/src/utils/cors.ts index e8de673..fbcfec9 100644 --- a/src/utils/cors.ts +++ b/src/utils/cors.ts @@ -9,11 +9,11 @@ export function setCORSHeaders(response: Response, request: Request): void { response.headers.set("Access-Control-Allow-Methods", SUPPORT_METHODS.join(", ")); response.headers.set( "Access-Control-Allow-Headers", - ["Authorization", "Content-Type", "Depth", "Overwrite", "Destination", "Range"].join(", ") + ["Authorization", "Content-Type", "Depth", "OverDestination", "If-Match", "If-None-Match"].join(", ") ); response.headers.set( "Access-Control-Expose-Headers", - ["Content-Type", "Content-Length", "DAV", "ETag", "Last-Modified", "Location", "Date", "Content-Range"].join(", ") + ["DAV", "ETag", "Last-Modified"].join(", ") ); response.headers.set("Access-Control-Allow-Credentials", "true"); response.headers.set("Access-Control-Max-Age", "86400"); diff --git a/src/utils/webdavUtils.ts b/src/utils/webdavUtils.ts index 0974953..b02cb66 100644 --- a/src/utils/webdavUtils.ts +++ b/src/utils/webdavUtils.ts @@ -1,11 +1,11 @@ import { WebDAVProps } from '../types'; -export async function* listAll(bucket: R2Bucket, prefix: string, isRecursive = false) { +export async function* listAll(bucket: R2Bucket, prefix: string) { let cursor: string | undefined = undefined; do { const r2_objects = await bucket.list({ prefix, - delimiter: isRecursive ? undefined : "/", + delimiter: "/", cursor, include: ["httpMetadata", "customMetadata"] }); @@ -13,78 +13,54 @@ export async function* listAll(bucket: R2Bucket, prefix: string, isRecursive = f yield object; } for (const prefix of r2_objects.delimitedPrefixes) { - yield { key: prefix, customMetadata: { resourcetype: 'collection' } } as R2Object; + yield { key: prefix, customMetadata: { resourcetype: "collection" } } as R2Object; } - cursor = r2_objects.truncd ? r2_objects.cursor : undefined; + cursor = r2_objects.truncated ? r2_objects.cursor : undefined; } while (cursor); } -export function fromR2Object(object: R2Object, bucketName: string): WebDAVProps { - const isCollection = object.key.endsWith('/') || object.customMetadata?.resourcetype === 'collection'; - const displayName = object.key.split('/').pop() || object.key; +export function fromR2Object(object: R2Object, basePath: string): WebDAVProps { + const isCollection = object.customMetadata?.resourcetype === "collection"; + const relativePath = object.key.slice(basePath.length); return { - href: `/${bucketName}/${object.key}${isCollection ? '/' : ''}`, - creationdate: object.uploaded?.toISOString() || new Date().toISOString(), - displayname: displayName, - getcontentlanguage: object.httpMetadata?.contentLanguage || '', - getcontentlength: isCollection ? '0' : object.size?.toString() || '0', - getcontenttype: isCollection ? 'httpd/unix-directory' : (object.httpMetadata?.contentType || 'application/octet-stream'), - getetag: object.etag || `"${new Date().getTime().toString(16)}"`, - getlastmodified: object.uploaded?.toUTCString() || new Date().toUTCString(), - resourcetype: isCollection ? 'collection' : '' + creationdate: object.uploaded?.toISOString() ?? new Date().toISOString(), + displayname: relativePath || "/", + getcontentlength: isCollection ? "0" : object.size.toString(), + getcontenttype: isCollection ? "httpd/unix-directory" : (object.httpMetadata?.contentType ?? "application/octet-stream"), + getetag: object.etag ?? "", + getlastmodified: object.uploaded?.toUTCString() ?? new Date().toUTCString(), + resourcetype: isCollection ? "" : "", + iscollection: isCollection }; } export function make_resource_path(request: Request): string { - let path = new URL(request.url).pathname.slice(1); - return path.endsWith("/") ? path.slice(0, -1) : path; + return new URL(request.url).pathname.slice(1); } export function generatePropfindResponse(bucketName: string, basePath: string, props: WebDAVProps[]): string { const xml = ` -${props.map(prop => generatePropResponse(prop)).join('\n')} +${props.map(prop => generatePropResponse(bucketName, basePath, prop)).join('\n')} `; return xml; } -function generatePropResponse(prop: WebDAVProps): string { +function generatePropResponse(bucketName: string, basePath: string, prop: WebDAVProps): string { + const resourcePath = `/${bucketName}/${basePath}${prop.displayname}${prop.iscollection ? '/' : ''}`; return ` - ${escapeXml(prop.href)} + ${resourcePath} ${prop.creationdate} - ${escapeXml(prop.displayname)} - ${escapeXml(prop.getcontentlanguage)} + ${prop.displayname} ${prop.getcontentlength} - ${escapeXml(prop.getcontenttype)} - ${escapeXml(prop.getet:getetag> + ${prop.getcontenttype} + ${prop.getetag} ${prop.getlastmodified} - ${prop.resourcetype === 'collection' ? '' : ''} - - - - - - - - - - + ${prop.resourcetype} HTTP/1.1 200 OK `; -} - -function escapeXml(unsafe: string): string {replace(/[<>&'"]/g, function (c) { - switch (c) { - case '<': return '<'; - case '>': return '>'; - case '&': return '&'; - case '\'': return '''; - case '"': return '"'; - } - return c; - }); } \ No newline at end of file