Refactor index.ts

This commit is contained in:
abersheeran 2023-12-05 15:16:18 +08:00
parent 316c90b62c
commit 23cfd81123
3 changed files with 1270 additions and 1333 deletions

2375
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,16 @@
{ {
"name": "r2-webdav", "name": "r2-webdav",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"deploy": "wrangler deploy", "deploy": "wrangler deploy",
"dev": "wrangler dev", "dev": "wrangler dev",
"start": "wrangler dev" "start": "wrangler dev"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20230419.0", "@cloudflare/workers-types": "^4.20230419.0",
"@types/xml2js": "^0.4.14", "typescript": "^5.0.4",
"typescript": "^5.0.4", "wrangler": "^3.0.0"
"wrangler": "^3.0.0" },
}, "dependencies": {}
"dependencies": {
"xml2js": "^0.6.2"
}
} }

View File

@ -29,13 +29,10 @@ export interface Env {
PASSWORD: string; PASSWORD: string;
} }
import * as xml2js from 'xml2js';
const DAV_CLASS = "1"; const DAV_CLASS = "1";
const SUPPORT_METHODS = [ const SUPPORT_METHODS = [
"OPTIONS", "OPTIONS",
"PROPFIND", "PROPFIND",
"PROPPATCH",
"MKCOL", "MKCOL",
"GET", "GET",
"HEAD", "HEAD",
@ -95,7 +92,7 @@ export default {
let response: Response; let response: Response;
let resource_path = new URL(request.url).pathname; let resource_path = new URL(request.url).pathname.slice(1);
switch (request.method) { switch (request.method) {
case 'OPTIONS': { case 'OPTIONS': {
@ -113,9 +110,13 @@ export default {
if (request.url.endsWith('/')) { if (request.url.endsWith('/')) {
let r2_objects = await bucket.list({ let r2_objects = await bucket.list({
prefix: resource_path, prefix: resource_path,
delimiter: '/',
}); });
let page = ''; let page = '';
for (let object of r2_objects.objects) { for (let dirname of r2_objects.delimitedPrefixes) {
page += `<a href="${dirname}">${dirname}</a><br>`;
}
for (let object of r2_objects.objects.filter(object => !object.key.endsWith('/'))) {
page += `<a href="${object.key}">${object.httpMetadata?.contentDisposition ?? object.key}</a><br>`; page += `<a href="${object.key}">${object.httpMetadata?.contentDisposition ?? object.key}</a><br>`;
} }
response = new Response(page, { status: 200, headers: { 'Content-Type': 'text/html' } }); response = new Response(page, { status: 200, headers: { 'Content-Type': 'text/html' } });
@ -166,17 +167,20 @@ export default {
response = new Response('Method Not Allowed', { status: 405 }); response = new Response('Method Not Allowed', { status: 405 });
break; break;
} }
// Check if the parent directory exists
let dirpath = resource_path.split('/').slice(0, -1).join('/') + '/'; let dirpath = resource_path.split('/').slice(0, -1).join('/') + '/';
if (await bucket.head(dirpath)) { let dir = await bucket.head(dirpath);
let body = await request.arrayBuffer(); if (!dir || dir.customMetadata?.resourcetype !== '<collection />') {
await bucket.put(resource_path, body, {
onlyIf: request.headers,
httpMetadata: request.headers,
});
response = new Response('', { status: 201 });
} else {
response = new Response('Conflict', { status: 409 }); response = new Response('Conflict', { status: 409 });
} }
let body = await request.arrayBuffer();
await bucket.put(resource_path, body, {
onlyIf: request.headers,
httpMetadata: request.headers,
});
response = new Response('', { status: 201 });
} }
break; break;
case 'DELETE': { case 'DELETE': {
@ -196,28 +200,55 @@ export default {
case 'MKCOL': { case 'MKCOL': {
if (request.body) { if (request.body) {
response = new Response('Unsupported Media Type', { status: 415 }); response = new Response('Unsupported Media Type', { status: 415 });
} else { break;
let parent_dir = resource_path.split('/').slice(0, -2).join("/") + '/';
if (!resource_path.endsWith('/')) {
response = new Response('Forbidden', { status: 403 });
} else if (await bucket.head(resource_path)) {
response = new Response('Method Not Allowed', { status: 405 });
} else if (parent_dir !== '/' && !await bucket.head(parent_dir)) {
response = new Response('Conflict', { status: 409 });
} else {
await bucket.put(resource_path, new Uint8Array(), {
httpMetadata: request.headers,
});
response = new Response('', { status: 201 });
}
} }
resource_path = resource_path.endsWith('/') ? resource_path : resource_path + '/';
// Check if the resource already exists
if (await bucket.head(resource_path)) {
response = new Response('Method Not Allowed', { status: 405 });
break;
}
// Check if the parent directory exists
let parent_dir = resource_path.split('/').slice(0, -2).join("/") + '/';
if (parent_dir !== '/' && !await bucket.head(parent_dir)) {
response = new Response('Conflict', { status: 409 });
break;
}
await bucket.put(resource_path, new Uint8Array(), { httpMetadata: request.headers });
response = new Response('', { status: 201 });
} }
break; break;
case 'PROPFIND': { case 'PROPFIND': {
let depth = request.headers.get('Depth') ?? 'infinity'; let depth = request.headers.get('Depth') ?? 'infinity';
switch (depth) { switch (depth) {
case '0': { case '0': {
if (resource_path === "") {
response = new Response(`<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:">
<response>
<href>${resource_path}</href>
<propstat>
<prop>
<resourcetype><collection /></resourcetype>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>
`, {
status: 207,
headers: {
'Content-Type': 'text/xml',
},
});
break;
}
let object = await bucket.get(resource_path); let object = await bucket.get(resource_path);
if (object === null && !resource_path.endsWith('/')) { if (object === null && !resource_path.endsWith('/')) {
response = new Response('Not Found', { status: 404 }); response = new Response('Not Found', { status: 404 });
@ -237,88 +268,70 @@ export default {
<status>HTTP/1.1 200 OK</status> <status>HTTP/1.1 200 OK</status>
</propstat> </propstat>
</response> </response>
</multistatus>`; </multistatus>
`;
response = new Response(page, { response = new Response(page, {
status: 207, status: 207,
headers: { headers: {
'Content-Type': 'text/xml', 'Content-Type': 'text/xml',
}, },
}); });
console.log(await request.text(), page);
} }
} }
break; break;
case '1': { case '1': {
let r2_objects = await bucket.list({
prefix: resource_path,
});
let page = `<?xml version="1.0" encoding="utf-8"?> let page = `<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:">`; <multistatus xmlns="DAV:">`;
for (let object of r2_objects.objects.filter(
object => { let cursor: string | undefined = undefined;
let path = object.key.slice(resource_path.length); do {
return !path.includes('/') || var r2_objects = await bucket.list({
(path.split('/').length === 2 && path.endsWith('/')); prefix: resource_path,
delimiter: '/',
cursor: cursor,
});
for (let dirname of r2_objects.delimitedPrefixes) {
page += `
<response>
<href>${dirname}</href>
<propstat>
<prop>
<resourcetype><collection /></resourcetype>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>`;
} }
)) {
page += ` for (let object of r2_objects.objects.filter(object => !object.key.endsWith('/'))) {
page += `
<response> <response>
<href>${object.key}</href> <href>${object.key}</href>
<propstat> <propstat>
<prop> <prop>
${Object.entries(fromR2Object(object)) ${Object.entries(fromR2Object(object))
.filter(([_, value]) => value !== undefined) .filter(([_, value]) => value !== undefined)
.map(([key, value]) => `<${key}>${value}</${key}>`) .map(([key, value]) => `<${key}>${value}</${key}>`)
.join('\n') .join('\n ')
} }
</prop> </prop>
<status>HTTP/1.1 200 OK</status> <status>HTTP/1.1 200 OK</status>
</propstat> </propstat>
</response>`; </response>`;
} }
page += '</multistatus>';
if (r2_objects.truncated) {
cursor = r2_objects.cursor;
}
} while (r2_objects.truncated)
page += '\n</multistatus>\n';
response = new Response(page, { response = new Response(page, {
status: 207, status: 207,
headers: { headers: {
'Content-Type': 'text/xml', 'Content-Type': 'text/xml',
}, },
}); });
console.log(await request.text(), page);
}
break;
case 'infinity': {
let r2_objects = await bucket.list({
prefix: resource_path,
});
let page = `<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:">`;
for (let object of r2_objects.objects) {
page += `
<response>
<href>${object.key}</href>
<propstat>
<prop>
${Object.entries(fromR2Object(object))
.filter(([_, value]) => value !== undefined)
.map(([key, value]) => `<${key}>${value}</${key}>`)
.join('\n')
}
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>`;
}
page += '</multistatus>';
response = new Response(page, {
status: 207,
headers: {
'Content-Type': 'text/xml',
},
});
console.log(await request.text(), page);
} }
break; break;
default: { default: {
@ -460,26 +473,6 @@ export default {
} }
} }
} }
case 'PROPPATCH': {
let request_xml = await xml2js.parseStringPromise(await request.text());
let setCustomMetadata = request_xml["D:propertyupdate"]["D:set"]["D:prop"];
let removeCustomMetadataKeys = Object.keys(request_xml["D:propertyupdate"]["D:remove"]["D:prop"]);
let object = await bucket.get(resource_path);
if (object === null) {
response = new Response('Not Found', { status: 404 });
} else {
await bucket.put(resource_path, object.body, {
httpMetadata: object.httpMetadata,
customMetadata: {
...Object.fromEntries(Object.entries(object.customMetadata ?? {}).filter(
([name, _]) => !removeCustomMetadataKeys.includes(name)
)),
...setCustomMetadata,
},
});
response = new Response('', { status: 200 });
}
}
default: { default: {
response = new Response('Method Not Allowed', { response = new Response('Method Not Allowed', {
status: 405, status: 405,