Skip to content

Commit

Permalink
add
Browse files Browse the repository at this point in the history
  • Loading branch information
aigem committed Sep 11, 2024
1 parent eed7863 commit a14c0d6
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 151 deletions.
4 changes: 2 additions & 2 deletions src/handlers/requestHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Response> {
export async function handleRequest(request: Request, env: ExecutionContext): Promise<Response> {
try {
if (request.method !== "OPTIONS" && !authenticate(request, env)) {
return new Response("Unauthorized", {
Expand Down
147 changes: 65 additions & 82 deletions src/handlers/webdavHandler.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -89,81 +89,74 @@ async function handleGet(request: Request, bucket: R2Bucket): Promise<Response>
});
}

async function handlePut(request: Request, bucket: R2Bucket): Promise<Response> {
async function handlePut( bucket: R2Bucket): Promise<Response> {
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<Response> {
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<Response> {
async function handleMkcol(request: Request, bucket: R2Bucket): Promise<Response> {
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<Response> {
asyncfind(request: Request, bucket: R2Bucket, bucketName: string): Promise<Response> {
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<Response> {
Expand All @@ -175,22 +168,17 @@ async function handleCopy(request: Request, bucket: R2Bucket): Promise<Response>
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<Response> {
Expand All @@ -202,22 +190,17 @@ async function handleMove(request: Request, bucket: R2Bucket): Promise<Response>
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 });
}
15 changes: 5 additions & 10 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
7 changes: 0 additions & 7 deletions src/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions src/utils/cors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
72 changes: 24 additions & 48 deletions src/utils/webdavUtils.ts
Original file line number Diff line number Diff line change
@@ -1,90 +1,66 @@
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"]
});
for (const object of r2_objects.objects) {
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 ? "<D:collection/>" : "",
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 = `<?xml version="1.0" encoding="utf-8"?>
<D:multistatus xmlns:D="DAV:">
${props.map(prop => generatePropResponse(prop)).join('\n')}
${props.map(prop => generatePropResponse(bucketName, basePath, prop)).join('\n')}
</D:multistatus>`;
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 ` <D:response>
<D:href>${escapeXml(prop.href)}</D:href>
<D:href>${resourcePath}</D:href>
<D:propstat>
<D:prop>
<D:creationdate>${prop.creationdate}</D:creationdate>
<D:displayname>${escapeXml(prop.displayname)}</D:displayname>
<D:getcontentlanguage>${escapeXml(prop.getcontentlanguage)}</D:getcontentlanguage>
<D:displayname>${prop.displayname}</>
<D:getcontentlength>${prop.getcontentlength}</D:getcontentlength>
<D:getcontenttype>${escapeXml(prop.getcontenttype)}</D:getcontenttype>
<D:getetag>${escapeXml(prop.getet:getetag>
<D:getcontenttype>${prop.getcontenttype}</D:getcontenttype>
<D:getetag>${prop.getetag}</D:getetag>
<D:getlastmodified>${prop.getlastmodified}</D:getlastmodified>
<D:resourcetype>${prop.resourcetype === 'collection' ? '<D:collection/>' : ''}</D:resourcetype>
<D:supportedlock>
<D:lockentry>
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
<D:lockentry>
<D:lockscope><D:shared/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
</D:supportedlock>
<D:resourcetype>${prop.resourcetype}</D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>`;
}

function escapeXml(unsafe: string): string {replace(/[<>&'"]/g, function (c) {
switch (c) {
case '<': return '&lt;';
case '>': return '&gt;';
case '&': return '&amp;';
case '\'': return '&apos;';
case '"': return '&quot;';
}
return c;
});
}

0 comments on commit a14c0d6

Please sign in to comment.