Skip to content

Commit

Permalink
updata
Browse files Browse the repository at this point in the history
  • Loading branch information
aigem committed Sep 11, 2024
1 parent a14c0d6 commit 5868b48
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 106 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/logger';
import { logger } from '../utils/logger';

export async function handleRequest(request: Request, env: ExecutionContext): Promise<Response> {
export async function handleRequest(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
try {
if (request.method !== "OPTIONS" && !authenticate(request, env)) {
return new Response("Unauthorized", {
Expand Down
266 changes: 191 additions & 75 deletions src/handlers/webdavHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { listAll, fromR2Object, make_resource_path, generatePropfindResponse } from '../utils/webdavUtils';
import { listAll, fromR2Object, make_resource_path, generatePropfindResponse, generateMultiStatus } from '../utils/webdavUtils';
import { logger } from '../utils/logger';
import { WebDAVProps } from '../types';

Expand All @@ -13,7 +13,7 @@ export async function handleWebDAV(request: Request, bucket: R2Bucket, bucketNam
case "HEAD":
return await handleHead(request, bucket);
case "GET":
return await handleGet(request, bucket);
return await handleGet(request, bucket, bucketName);
case "PUT":
return await handlePut(request, bucket);
case "DELETE":
Expand All @@ -31,7 +31,7 @@ export async function handleWebDAV(request: Request, bucket: R2Bucket, bucketNam
status: 405,
headers: {
Allow: SUPPORT_METHODS.join(", "),
DAV: DAV_CLASS
DA_CLASS
}
});
}
Expand Down Expand Up @@ -59,25 +59,38 @@ async function handleHead(request: Request, bucket: R2Bucket): Promise<Response>
return new Response(null, { status: 404 });
}

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()
}
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()
});

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): Promise<Response> {
async function handleGet(request: Request, bucket: R2Bucket, bucketName: string): Promise<Response> {
const resource_path = make_resource_path(request);
const object = await bucket.get(resource_path);

const object = await bucket.get(resource_path);
if (!object) {
return new Response("Not Found", { status: 404 });
}

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:
headers: { "Content-Type": "text/html; charset=utf-8" }
});
}

return new Response(object.body, {
status: 200,
headers: {
Expand All @@ -89,35 +102,45 @@ async function handleGet(request: Request, bucket: R2Bucket): Promise<Response>
});
}

async function handlePut( bucket: R2Bucket): Promise<Response> {
async function handlePut(request: Request, 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");

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 });
}
try {
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 });
} catch (error) {
logger.error("Error uploading file:", error);
return new Response("Internal Server Error", { status: 500 });
}

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);
await bucket.delete(resource_path);
return new Response(null, { status: 204 });

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);
}
}

await bucket.delete(resource_path);
return new Response(: 204 });
} catch (error) {
logger.error("Error deleting object:", error);
return new Response("Internal Server Error", { status: 500 });
}
}

async function handleMkcol(request: Request, bucket: R2Bucket): Promise<Response> {
Expand All @@ -127,36 +150,53 @@ async function handleMkcol(request: Request, bucket: R2Bucket): Promise<Response
return new Response("Method Not Allowed", { status: 405 });
}

await bucket.put(resource_path + "/.keep", new Uint8Array(), {
customMetadata: { resourcetype: "collection" }
});
try {
const existingObject = await bucket.head(resource_path);
if (existingObject) {
return new Response("Method Not Allowed", { status: 405 });
}

return new Response(null, { status: 201 });
await bucket.put(resource_path + "/.keep", new Uint8Array(), {
customMetadata: { resourcetype: "collection" }
});
return new Response(null, { status: 201 });
} catch (error) {
logger.error("Error creating collection:", error);
return new Response("Internal Server Error", { status: 500 });
}
}

asyncfind(request: Request, bucket: R2Bucket, bucketName: string): Promise<Response> {
async function handlePropfind(request: Request, bucket: R2Bucket, bucketName: string): Promise<Response> {
const resource_path = make_resource_path(request);
const depth = request.headers.get("Depth") || "infinity";
const depth = request.headersDepth") || "infinity";

const props: WebDAVProps[] = [];
if (depth !== "0") {
for await (const object of listAll(bucket, resource_path)) {
props.push(fromR2Object(object, resource_path));
}
} else {
try {
const props: WebDAVProps[] = [];
const object = await bucket.head(resource_path);
if (object) {
props.push(fromR2Object(object, resource_path));
} else {

if (!object) {
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" }
});
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));
}
}
}

const xml = generatePropfindResponse(bucketName, resource_path, props);
xml, {
status:,
headers: { "Content-Type": "application/xml; charset=utf-8" }
});
} catch (error) {
logger.error("Error in PROPFIND:", error);
return new Response("Internal Server Error", { status: 500 });
}
}

async function handleCopy(request: Request, bucket: R2Bucket): Promise<Response> {
Expand All @@ -168,17 +208,34 @@ async function handleCopy(request: Request, bucket: R2Bucket): Promise<Response>
const destinationUrl = new URL(destinationHeader);
const destinationPath = make_resource_path(new Request(destinationUrl));

const sourceObject = await bucket.get(sourcePath);
if (!sourceObject) {
return new Response("Not Found", { status: 404 });
}
try {
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
});
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
});
}

// Copy file
await bucket.put(destinationPath, sourceObject.body, {
httpMetadata: sourceObject.httpMetadata,
customMetadata: sourceObject.customMetadata
});
}

return new Response(null, { status: 201 });
return new Response(null, { status: 201 });
} catch (error) {
logger.error("Error in COPY:", error);
return new Response("Internal Server Error", { status: 500 });
}
}

async function handleMove(request: Request, bucket: R2Bucket): Promise<Response> {
Expand All @@ -190,17 +247,76 @@ async function handleMove(request: Request, bucket: R2Bucket): Promise<Response>
const destinationUrl = new URL(destinationHeader);
const destinationPath = make_resource_path(new Request(destinationUrl));

const sourceObject = await bucket.get(sourcePath);
if (!sourceObject) {
return new Response("Not Found", { status: 404 });
}
try {
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
});
if (sourceObject.customMetadata?.resourcetype === "collection") {
// Move 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
});
await bucket.delete(item.key);
}
} else {
// Move file
await bucket.put(destinationPath, sourceObject.body, {
httpMetadata: sourceObject.httpMetadata,
customMetadata: sourceObject.customMetadata
});
await bucket.delete(sourcePath);
}

return new Response(null, { 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<R2Object[]> {
const items: R2Object[] = [];
for await (const item of listAll(bucket, prefix)) (item.key !== prefix) {
items.push(item);
}
}
return items;
}

await bucket.delete(sourcePath);
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 `<li><a href="${href}">${isDirectory ? '📁 ' : '📄 '}${name}</a></li>`;
}).join('\n');

return new Response(null, { status: 204 });
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Directory Listing for /${bucketName}/${path}</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6;: 20px; }
h1 { color: #333; }
ul { list-style-type: none; padding: 0; } margin-bottom: 10px; }
a { color:066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>Directory Listing for /${bucketName}/${path}</h1>
<ul>
${path !== '' ? '<li><a href="../">📁 ..</a></li>' : ''}
${listItems}
</ul>
</body>
</html>
`;
}
11 changes: 8 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ export interface Env {
BUCKET_NAME: string;
}

export interface CacheableResponse {
response: Response;
expiry: number;
}

export interface WebDAVProps {
creationdate: string;
displayname: string;
getcontentlanguage: string | undefined;
getcontentlength: string;
getcontenttype: string;
getetag: string;
getcontenttype: string | undefined;
getetag: string | undefined;
getlastmodified: string;
resourcetype: string;
iscollection: boolean;
}
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", "OverDestination", "If-Match", "If-None-Match"].join(", ")
["Authorization", "Content-Type", "Depth", "Overwrite", "Destination", "Range"].join(", ")
);
response.headers.set(
"Access-Control-Expose-Headers",
["DAV", "ETag", "Last-Modified"].join(", ")
["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");
Expand Down
Loading

0 comments on commit 5868b48

Please sign in to comment.