diff --git a/src/handlers/webdavHandler.ts b/src/handlers/webdavHandler.ts index d3fe384..4787804 100644 --- a/src/handlers/webdavHandler.ts +++ b/src/handlers/webdavHandler.ts @@ -1,6 +1,7 @@ -import { listAll, fromR2Object, make_resource_path, generatePropfindResponse, generateMultiStatus } from '../utils/webdavUtils'; +import { listAll, fromR2Object, make_resource_path, generatePropfindResponse } from '../utils/webdavUtils'; import { logger } from '../utils/logger'; - { WebDAVProps } from '../types'; +import { generateHTML, generateErrorHTML } from '../utils/templates'; +import { WebDAVProps } from '../types'; const SUPPORT_METHODS = ["OPTIONS", "PROPFIND", "MKCOL", "GET", "HEAD", "PUT", "COPY", "MOVE", "DELETE"]; const DAV_CLASS = "1, 2"; @@ -37,11 +38,15 @@ export async function handleWebDAV(request: Request, bucket: R2Bucket, bucketNam } } catch (error) { logger.error("Error in WebDAV handling:", error); - return new Response("Internal Server Error", { status: 500 }); + return new Response(generateErrorHTML("Internal Server Error", error.message), { + status: 500, + headers: { "Content-Type": "text/html; charset=utf-8" } + }); } } -function handleOptions(): Response return new Response(null, { +function handleOptions(): Response { + return new Response(null, { status: 200, headers: { Allow: SUPPORT_METHODS.join(", "), @@ -58,49 +63,83 @@ async function handleHead(request: Request, bucket: R2Bucket): Promise return new Response(null, { status: 404 }); } - const headers = new Headers({ - "Content-Type": object.httpMetadata?.contentType ?? "application/octet-stream", - "Content-Length": object.size.toString(), - "ETag": object.etag, - "Last-Modified": object.uploaded.toUTCString() + return new Response(null, { + status: 200, + headers: { + "Content-Type": object.httpMetadata?.contentType ?? "application/octet-stream", + "Content-Length": object.size.toString(), + "ETag": object.etag, + "Last-Modified": object.uploaded.toUTCString() + } }); - - if (object.customMetadata?.resourcetype === "collection") { - headers.set("Content-Type", "httpd/unix-directory"); - } - - return new Response(null, { status: 200, headers }); } async function handleGet(request: Request, bucket: R2Bucket, bucketName: string): Promise { const resource_path = make_resource_path(request); - const object = await bucket.get(resource_path); - if (!object) { - return new Response("Not Found", { status: }); + if (request.url.endsWith("/")) { + // 处理目录 + return await handleDirectory(bucket, resource_path, bucketName); + } else { + // 处理文件 + return await handleFile(bucket, resource_path); } +} - if (object.customMetadata?.resourcetype === "collection") { - // Return a directory listing - const items = await listDirectoryContents(bucket, resource_path); - const html = generateDirectoryListing(bucketName, resource_path, items); - return new Response(html, { - status: 200, +async function handleDirectory(bucket: R2Bucket, resource_path: string, bucketName: string): Promise { + let items = []; + + if (resource_path !== "") { + items.push({ name: "📁 ..", href: "../" }); + } + + try { + for await (const object of listAll(bucket, resource_path)) { + if (object.key === resource_path) continue; + const isDirectory = object.customMetadata?.resourcetype === ""; + const displayName = object.key.split('/').pop() || object.key; + const href = `/${object.key + (isDirectory ? "/" : "")}`; + items.push({ name: `${isDirectory ? '📁 ' : '📄 '}${displayName}`, href }); + } + } catch (error) { + logger.error("Error listing objects:", error); + return new Response(generateErrorHTML("Error listing directory contents", error.message), { + status: 500, headers: { "Content-Type": "text/html; charset=utf-8" } }); } - return new Response(object.body, { + const page = generateHTML("WebDAV File Browser", items); + return new Response(page, { status: 200, - headers: { - "Content-Type": object.httpMetadata?.contentType ?? "application/octet-stream", - "Content-Length": object.size.toString(), - "ETag": object.etag, - "Last-Modified": object.uploaded.toUTCString() - } + headers: { "Content-Type": "text/html; charset=utf-8" } }); } +async function handleFile(bucket: R2Bucket, resource_path: string): Promise { + try { + const object = await bucket.get(resource_path); + if (!object) { + return new Response("Not Found", { status: 404 }); + } + return new Response(object.body, { + status: 200, + headers: { + "Content-Type": object.httpMetadata?.contentType ?? "application/octet-stream", + "Content-Length": object.size.toString(), + "ETag": object.etag, + "Last-Modified": object.uploaded.toUTCString() + } + }); + } catch (error) { + logger.error("Error getting object:", error); + return new Response(generateErrorHTML("Error retrieving file", error.message), { + status: 500, + headers: { "Content-Type": "text/html; charset=utf-8" } + }); + } +} + async function handlePut(request: Request, bucket: R2Bucket): Promise { const resource_path = make_resource_path(request); @@ -111,33 +150,28 @@ async function handlePut(request: Request, bucket: R2Bucket): Promise contentType: request.headers.get("Content-Type") || "application/octet-stream", }, }); - return new Response(null, { status: 201 }); + return new Response("Created", { status: 201 }); } catch (error) { logger.error("Error uploading file:", error); - return new Response("Internal Server Error", { status: 500 }); + return new Response(generateErrorHTML("Error uploading file", error.message), { + status: 500, + headers: { "Content-Type": "text/html; charset=utf-8" } + }); } } async function handleDelete(request: Request, bucket: R2Bucket): Promise { - const resource_path = make_resource_path); + const resource_path = make_resource_path(request); try { - const object = await bucket.head(resource_path); - if (!object) { - return new Response("Not Found", { status: 404 }); - } - - if (object.customMetadata?.resourcetype === "collection") { - // Delete all objects within the directory - for await (const item of listAll(bucket, resource_path, true)) await bucket.delete(item.key); - } - } - - bucket.delete(resource_path); - return new Response(null, { status: 204 }); + await bucket.delete(resource_path); + return new Response("No Content", { status: 204 }); } catch (error) { logger.error("Error deleting object:", error); - return new Response("Internal Server Error", { status: 500 }); + return new Response(generateErrorHTML("Error deleting file", error.message), { + status: 500, + headers: { "Content-Type": "text/html; charset=utf-8" } + }); } } @@ -145,44 +179,39 @@ async function handleMkcol(request: Request, bucket: R2Bucket): Promise { +async function handlePropfind(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[] = []; - const object = await bucket.head(resource_path); - - if (!object) { - return new Response("Not Found", { status: 404 }); - } - - props.push(fromR2Object(object)); - - if (depth !== "0" && object.customMetadata?.resourcetype === "collection") { - for await (const item of listAll(bucket, resource_path { - if (item.key !== resource_path) { - props.push(fromR2Object(item)); - } + if (depth !== "0") { + for await (const object of listAll(bucket, resource_path)) { + props.push(fromR2Object(object)); + } + } else { + const object = await bucket.head(resource_path); + if (object) { + props.push(fromR2Object(object)); + } else { + return new Response("Not Found", { status: 404 }); } } @@ -193,7 +222,10 @@ async function handlefind(request: Request, bucket: R2Bucket, bucketName: string }); } catch (error) { logger.error("Error in PROPFIND:", error); - return new Response("Internal Server Error", { status: 500 }); + return new Response(generateErrorHTML("Error in PROPFIND", error.message), { + status: 500, + headers: { "Content-Type": "text/html; charset=utf-8" } + }); } } @@ -212,27 +244,18 @@ async function handleCopy(request: Request, bucket: R2Bucket): Promise return new Response("Not Found", { status: 404 }); } - if (sourceObject.customMetadata?.resourcetype === "collection") { - // Copy directory - for await (const item of listAll(bucket, sourcePath, true)) { - const newPath = item.key.replace(sourcePath, destinationPath); - await bucket.put(newPath, await (await bucket.get(item.key))!.arrayBuffer(), { - httpMetadata: item.httpMetadata, - customMetadata: item.customMetadata - }); - } - } else { - // Copy file - await bucket.put(destinationPath,Object.body, { - httpMetadata: sourceObject.httpMetadata, - customMetadata: sourceObject.customMetadata - }); - } + await bucket.put(destinationPath, sourceObject.body, { + httpMetadata: sourceObject.httpMetadata, + customMetadata: sourceObject.customMetadata + }); - return new Response(null, { status: 201 }); + 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(generateErrorHTML("Error copying file", error.message), { + status: 500, + headers: { "Content-Type": "text/html; charset=utf-8" } + }); } } @@ -251,73 +274,19 @@ async function handleMove(request: Request, bucket: R2Bucket): Promise return new Response("Not Found", { status: 404 }); } - if (sourceObject.customMetadata?.resourcetype === "collection") { - // Move directory - for await (const item of listAll(bucket, sourcePath, true)) { - const newPath = item.key.replace(sourcePath,Path); - await bucket.put(newPath, await (await bucket.get(item.key))!.arrayBuffer(), { - httpMetadata: item.httpMetadata, - customMetadata: item.customMetadata - }); - await bucket.delete(item.key); - } - } { - // Move file - await bucket.put(destinationPath, sourceObject.body, { - httpMetadata: sourceObject.httpMetadata, - customMetadata: sourceObject.customMetadata - }); - await bucket.delete(sourcePath); - } + await bucket.put(destinationPath, sourceObject.body, { + httpMetadata: sourceObject.httpMetadata, + customMetadata: sourceObject.customMetadata + }); + + await bucket.delete(sourcePath); - return new Response(null, { status: 204 }); + return new Response("No Content", { status: 204 }); } catch (error) { logger.error("Error in MOVE:", error); - return new Response("Internal Server Error", { status: 500 }); - } -} - -async function listDirectoryContents(bucket: R2Bucket, prefix: string): Promise { - const items: R2Object[] = []; - for await (const item of listAll(bucket, prefix)) { - if (item.key !== prefix) { - items.push(item); - } + return new Response(generateErrorHTML("Error moving file", error.message), { + status: 500, + headers: { "Content-Type": "text/html; charset=utf-8" } + }); } - return items; } - -function generateDirectoryListing(bucketName: string, path: string, items: R2Object[]): string { - const listItems = items.map(item => { - const isDirectory = item.customMetadata?.resourcetype === "collection"; - const name = item.key.split('/').pop() || item.key; - const href = `/${bucketName}/${item.key}${isDirectory ? '/' : ''}`; - return `
  • ${isDirectory ? '📁 ' : '📄 '}${name}
  • `; - }).join('\n'); - - return ` - - - - - - Directory Listing for /${bucketName}/${path} - - - -

    Directory Listing for /${bucketName}/${path}

    -
      - ${path !== '' ? '
    • 📁 ..
    • ' : ''} - ${listItems} -
    - - - `; -} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 4ac34f8..85f7af7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,7 +2,7 @@ export interface Env { BUCKET: R2Bucket; USERNAME: string; PASSWORD: string; - BUCKET_NAME: string; + BUCKET_NAME: string; // 新增的环境变量 } export interface CacheableResponse { @@ -12,7 +12,7 @@ export interface CacheableResponse { export interface WebDAVProps { creationdate: string; - displayname: string; + displayname: string | undefined; getcontentlanguage: string | undefined; getcontentlength: string; getcontenttype: string | undefined; diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 3c9bb09..32343f1 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,3 +1,10 @@ +// 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..257fd2a 100644 --- a/src/utils/cors.ts +++ b/src/utils/cors.ts @@ -1,20 +1,10 @@ -export function setCORSHeaders(response: Response, request: Request): void { - const SUPPORT_METHODS = ["OPTIONS", "PROPFIND", "MKCOL", "GET", "HEAD", "PUT", "COPY", "MOVE", "DELETE"]; - - const origin = request.headers.get("Origin"); - if (origin) { - response.headers.set("Access-Control-Allow-Origin", origin); +export const logger = { + info: (message: string, ...args: any[]) => console.log(`[INFO] ${new Date().toISOString()} - ${message}`, ...args), + error: (message: string, ...args: any[]) => console.error(`[ERROR] ${new Date().toISOString()} - ${message}`, ...args), + warn: (message: string, ...args: any[]) => console.warn(`[WARN] ${new Date().toISOString()} - ${message}`, ...args), + debug: (message: string, ...args: any[]) => { + if (process.env.NODE_ENV !== 'production') { + console.debug(`[DEBUG] ${new Date().toISOString()} - ${message}`, ...args); + } } - - 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(", ") - ); - response.headers.set( - "Access-Control-Expose-Headers", - ["Content-Type", "Content-Length", "DAV", "ETag", "Last-Modified", "Location", "Date", "Content-Range"].join(", ") - ); - response.headers.set("Access-Control-Allow-Credentials", "true"); - response.headers.set("Access-Control-Max-Age", "86400"); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/utils/logger.ts b/src/utils/logger.ts index df79156..257fd2a 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,6 +1,6 @@ export const logger = { info: (message: string, ...args: any[]) => console.log(`[INFO] ${new Date().toISOString()} - ${message}`, ...args), - error: (message: string, ...args: any[]) => console.error(`[ERROR] ${ISOString()} - ${message}`, ...args), + error: (message: string, ...args: any[]) => console.error(`[ERROR] ${new Date().toISOString()} - ${message}`, ...args), warn: (message: string, ...args: any[]) => console.warn(`[WARN] ${new Date().toISOString()} - ${message}`, ...args), debug: (message: string, ...args: any[]) => { if (process.env.NODE_ENV !== 'production') { diff --git a/src/utils/webdavUtils.ts b/src/utils/webdavUtils.ts index d2e97e1..0cb2982 100644 --- a/src/utils/webdavUtils.ts +++ b/src/utils/webdavUtils.ts @@ -19,64 +19,55 @@ export async function* listAll(bucket: R2Bucket, prefix: string, isRecursive = f export function fromR2Object(object: R2Object | null): WebDAVProps { if (!object) { return { - creationdate: new Date().toISOString(), - displayname: "", + creationdate: new Date().toUTCString(), + displayname: undefined, getcontentlanguage: undefined, getcontentlength: "0", - getcontenttype: "application/octet-stream", - getetag: "", + getcontenttype: undefined, + getetag: undefined, getlastmodified: new Date().toUTCString(), - resourcetype "" + resourcetype: "" }; } - const isCollection = object.customMetadata?.resourcetype === "collection"; return { - creationdate: object.uploaded.toISOString(), - displayname: object.key.split('/').pop() || object.key, + creationdate: object.uploaded.toUTCString(), + displayname: object.httpMetadata?.contentDisposition, getcontentlanguage: object.httpMetadata?.contentLanguage, - getcontentlength: isCollection ? "0" : object.size.toString(), - getcontenttype: isCollection ? "httpd/unix-directory" : (object.httpMetadata?.contentType || "application/octet-stream"), + getcontentlength: object.size.toString(), + getcontenttype: object.httpMetadata?.contentType, getetag: object.etag, getlastmodified: object.uploaded.toUTCString(), - resourcetype: isCollection ? "collection" : "" + resourcetype: object.customMetadata?.resourcetype ?? "" }; } -export function_resource_path(request: Request): string { +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; } export function generatePropfindResponse(bucketName: string, basePath: string, props: WebDAVProps[]): string { - const xml = `xml version="1.0" encoding="utf-8"?> - + const xml = ` + ${props.map(prop => generatePropResponse(bucketName, basePath, prop)).join('\n')} `; return xml; } function generatePropResponse(bucketName: string, basePath: string, prop: WebDAVProps): string { - const resourcePath = `/${bucketName}/${basePath}${prop.displayname ? '/' + prop.displayname : ''}`.replace\/+/g, '/'); + const resourcePath = `/${basePath}${prop.displayname ? '/' + prop.displayname : ''}`; return ` ${resourcePath} - ${prop.creationdate}${prop.displayname} - ${prop.getcontentlanguage || ''} + ${prop.creationdate} ${prop.getcontentlength} ${prop.getcontenttype || ''} ${prop.getetag || ''} - ${prop.getlastmodified} - ${prop.resourcetype === 'collection' ? '' : ''} + ${prop.getlastmodified} + ${prop.resourcetype ? '' : ''} HTTP/1.1 200 OK `; } - -export function generateMultiStatus(responses: string[]): string { - return `xml version="1.0" encoding="utf-8"?> - -${responses.join('\n')} -`; -} \ No newline at end of file