diff --git a/src/index.ts b/src/index.ts
index 020d50c..abd5e13 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -30,37 +30,24 @@ export interface Env {
}
async function* listAll(bucket: R2Bucket, prefix: string, isRecursive: boolean = false) {
- let cursor: string | undefined = undefined;
- do {
- var r2_objects = await bucket.list({
- prefix: prefix,
+ let cursor = undefined,
+ truncated = true;
+ while (truncated) {
+ const r2_objects = await bucket.list({
+ prefix,
+ cursor,
delimiter: isRecursive ? undefined : '/',
- cursor: cursor,
- include: ['httpMetadata', 'customMetadata'],
});
-
- for (let object of r2_objects.objects) {
+ for (const object of r2_objects.objects) {
yield object;
}
-
- if (r2_objects.truncated) {
- cursor = r2_objects.cursor;
- }
- } while (r2_objects.truncated)
+ if (!r2_objects.truncated) break;
+ cursor = r2_objects.cursor;
+ }
}
-
-const DAV_CLASS = "1";
-const SUPPORT_METHODS = [
- "OPTIONS",
- "PROPFIND",
- "MKCOL",
- "GET",
- "HEAD",
- "PUT",
- "COPY",
- "MOVE",
-];
+const DAV_CLASS = '1';
+const SUPPORT_METHODS = ['OPTIONS', 'PROPFIND', 'MKCOL', 'GET', 'HEAD', 'PUT', 'COPY', 'MOVE'];
type DavProperties = {
creationdate: string | undefined;
@@ -71,7 +58,7 @@ type DavProperties = {
getetag: string | undefined;
getlastmodified: string | undefined;
resourcetype: string;
-}
+};
function fromR2Object(object: R2Object | null | undefined): DavProperties {
if (object === null || object === undefined) {
@@ -79,11 +66,11 @@ function fromR2Object(object: R2Object | null | undefined): DavProperties {
creationdate: new Date().toUTCString(),
displayname: undefined,
getcontentlanguage: undefined,
- getcontentlength: "0",
+ getcontentlength: '0',
getcontenttype: undefined,
getetag: undefined,
getlastmodified: new Date().toUTCString(),
- resourcetype: "",
+ resourcetype: '',
};
}
@@ -99,20 +86,19 @@ function fromR2Object(object: R2Object | null | undefined): DavProperties {
};
}
-
function make_resource_path(request: Request): string {
let path = new URL(request.url).pathname.slice(1);
path = path.endsWith('/') ? path.slice(0, -1) : path;
return path;
}
-async function handle_options(request: Request, bucket: R2Bucket): Promise {
+async function handle_options(_request: Request, _bucket: R2Bucket): Promise {
return new Response(null, {
status: 204,
headers: {
- 'DAV': DAV_CLASS,
- 'Allow': SUPPORT_METHODS.join(', '),
- }
+ DAV: DAV_CLASS,
+ Allow: SUPPORT_METHODS.join(', '),
+ },
});
}
@@ -132,50 +118,45 @@ async function handle_get(request: Request, bucket: R2Bucket): Promise
let page = '';
if (resource_path !== '') page += `..
`;
for await (const object of listAll(bucket, resource_path)) {
- if (object.key === resource_path) {
- continue
- }
+ if (object.key === resource_path) continue;
let href = `/${object.key + (object.customMetadata?.resourcetype === '' ? '/' : '')}`;
page += `${object.httpMetadata?.contentDisposition ?? object.key}
`;
}
return new Response(page, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
} else {
- let object = await bucket.get(resource_path, {
+ const object = await bucket.get(resource_path, {
onlyIf: request.headers,
range: request.headers,
});
- let isR2ObjectBody = (object: R2Object | R2ObjectBody): object is R2ObjectBody => {
- return 'body' in object;
- }
+ const isR2ObjectBody = (object: R2Object | R2ObjectBody): object is R2ObjectBody => 'body' in object;
+
+ const range = request.headers.get('Range');
+ const contentRange = (range: string, object: R2ObjectBody) => {
+ const [start, end] = range
+ .replace(/bytes=/, '')
+ .split('-')
+ .map(Number);
+ return { 'Content-Range': `bytes ${start}-${end}/${object.size}` };
+ };
if (object === null) {
return new Response('Not Found', { status: 404 });
} else if (!isR2ObjectBody(object)) {
- return new Response("Precondition Failed", { status: 412 });
+ return new Response('Precondition Failed', { status: 412 });
} else {
return new Response(object.body, {
- status: object.range ? 206 : 200,
+ status: range ? 206 : 200,
headers: {
+ //
'Content-Type': object.httpMetadata?.contentType ?? 'application/octet-stream',
- // TODO: Content-Length, Content-Range
-
- ...(object.httpMetadata?.contentDisposition ? {
- 'Content-Disposition': object.httpMetadata.contentDisposition,
- } : {}),
- ...(object.httpMetadata?.contentEncoding ? {
- 'Content-Encoding': object.httpMetadata.contentEncoding,
- } : {}),
- ...(object.httpMetadata?.contentLanguage ? {
- 'Content-Language': object.httpMetadata.contentLanguage,
- } : {}),
- ...(object.httpMetadata?.cacheControl ? {
- 'Cache-Control': object.httpMetadata.cacheControl,
- } : {}),
- ...(object.httpMetadata?.cacheExpiry ? {
- 'Cache-Expiry': object.httpMetadata.cacheExpiry.toISOString(),
- } : {}),
- }
+ ...(range && contentRange(range, object)),
+ ...(object.httpMetadata?.contentDisposition && { 'Content-Disposition': object.httpMetadata.contentDisposition }),
+ ...(object.httpMetadata?.contentEncoding && { 'Content-Encoding': object.httpMetadata.contentEncoding }),
+ ...(object.httpMetadata?.contentLanguage && { 'Content-Language': object.httpMetadata.contentLanguage }),
+ ...(object.httpMetadata?.cacheControl && { 'Cache-Control': object.httpMetadata.cacheControl }),
+ ...(object.httpMetadata?.cacheExpiry && { 'Cache-Expiry': object.httpMetadata.cacheExpiry.toISOString() }),
+ },
});
}
}
@@ -209,18 +190,15 @@ async function handle_delete(request: Request, bucket: R2Bucket): Promise object.key);
- if (keys.length > 0) {
- await bucket.delete(keys);
- }
-
- if (r2_objects.truncated) {
- cursor = r2_objects.cursor;
- }
- } while (r2_objects.truncated);
+ let truncated = true,
+ cursor: string | undefined = undefined;
+ while (truncated) {
+ const r2_objects = await bucket.list({ cursor });
+ let keys = r2_objects.objects.map((object) => object.key);
+ if (keys.length > 0) await bucket.delete(keys);
+ if (!r2_objects.truncated) break;
+ cursor = r2_objects.cursor;
+ }
return new Response(null, { status: 204 });
}
@@ -234,21 +212,18 @@ async function handle_delete(request: Request, bucket: R2Bucket): Promise object.key);
- if (keys.length > 0) {
- await bucket.delete(keys);
- }
-
- if (r2_objects.truncated) {
- cursor = r2_objects.cursor;
- }
- } while (r2_objects.truncated);
+ let keys = r2_objects.objects.map((object) => object.key);
+ if (keys.length > 0) await bucket.delete(keys);
+ if (!r2_objects.truncated) break;
+ cursor = r2_objects.cursor;
+ }
return new Response(null, { status: 204 });
}
@@ -267,15 +242,15 @@ async function handle_mkcol(request: Request, bucket: R2Bucket): Promise' }
+ customMetadata: { resourcetype: '' },
});
return new Response('', { status: 201 });
}
@@ -287,20 +262,24 @@ async function handle_propfind(request: Request, bucket: R2Bucket): Promise
`;
- if (resource_path === "") {
- page += `
-
- /
+ const create_page = (href: string, object: R2Object | null) => {
+ const davPropertites = Object.entries(fromR2Object(object))
+ .filter(([_, value]) => value !== undefined)
+ .map(([key, value]) => `<${key}>${value}${key}>`)
+ .join('\n ');
+ return `
+ ${href}
- ${Object.entries(fromR2Object(null))
- .filter(([_, value]) => value !== undefined)
- .map(([key, value]) => `<${key}>${value}${key}>`)
- .join('\n ')}
+ ${davPropertites}
HTTP/1.1 200 OK
`;
+ };
+
+ if (resource_path === '') {
+ page += create_page('/', null);
is_collection = true;
} else {
let object = await bucket.head(resource_path);
@@ -309,65 +288,31 @@ async function handle_propfind(request: Request, bucket: R2Bucket): Promise';
let href = `/${object.key + (object.customMetadata?.resourcetype === '' ? '/' : '')}`;
- page += `
-
- ${href}
-
-
- ${Object.entries(fromR2Object(object))
- .filter(([_, value]) => value !== undefined)
- .map(([key, value]) => `<${key}>${value}${key}>`)
- .join('\n ')}
-
- HTTP/1.1 200 OK
-
- `
- };
+ page += create_page(href, object);
+ }
if (is_collection) {
let depth = request.headers.get('Depth') ?? 'infinity';
switch (depth) {
- case '0': break;
- case '1': {
- let prefix = resource_path === "" ? resource_path : resource_path + '/';
- for await (let object of listAll(bucket, prefix)) {
- let href = `/${object.key + (object.customMetadata?.resourcetype === '' ? '/' : '')}`;
- page += `
-
- ${href}
-
-
- ${Object.entries(fromR2Object(object))
- .filter(([_, value]) => value !== undefined)
- .map(([key, value]) => `<${key}>${value}${key}>`)
- .join('\n ')}
-
- HTTP/1.1 200 OK
-
- `;
- }
- }
+ case '0':
break;
- case 'infinity': {
- let prefix = resource_path === "" ? resource_path : resource_path + '/';
- for await (let object of listAll(bucket, prefix, true)) {
- let href = `/${object.key + (object.customMetadata?.resourcetype === '' ? '/' : '')}`;
- page += `
-
- ${href}
-
-
- ${Object.entries(fromR2Object(object))
- .filter(([_, value]) => value !== undefined)
- .map(([key, value]) => `<${key}>${value}${key}>`)
- .join('\n ')
- }
-
- HTTP/1.1 200 OK
-
- `;
+ case '1':
+ {
+ let prefix = resource_path === '' ? resource_path : resource_path + '/';
+ for await (let object of listAll(bucket, prefix)) {
+ let href = `/${object.key + (object.customMetadata?.resourcetype === '' ? '/' : '')}`;
+ page += create_page(href, object);
+ }
+ }
+ break;
+ case 'infinity':
+ {
+ let prefix = resource_path === '' ? resource_path : resource_path + '/';
+ for await (let object of listAll(bucket, prefix, true)) {
+ let href = `/${object.key + (object.customMetadata?.resourcetype === '' ? '/' : '')}`;
+ page += create_page(href, object);
+ }
}
- }
break;
default: {
return new Response('Forbidden', { status: 403 });
@@ -395,8 +340,11 @@ async function handle_copy(request: Request, bucket: R2Bucket): Promise {
- let target = destination + "/" + object.key.slice(prefix.length);
- target = target.endsWith("/") ? target.slice(0, -1) : target;
+ let target = destination + '/' + object.key.slice(prefix.length);
+ target = target.endsWith('/') ? target.slice(0, -1) : target;
let src = await bucket.get(object.key);
if (src !== null) {
await bucket.put(target, src.body, {
@@ -487,8 +435,11 @@ async function handle_move(request: Request, bucket: R2Bucket): Promise {
- let target = destination + "/" + object.key.slice(prefix.length);
- target = target.endsWith("/") ? target.slice(0, -1) : target;
+ let target = destination + '/' + object.key.slice(prefix.length);
+ target = target.endsWith('/') ? target.slice(0, -1) : target;
let src = await bucket.get(object.key);
if (src !== null) {
await bucket.put(target, src.body, {
@@ -611,9 +563,9 @@ async function dispatch_handler(request: Request, bucket: R2Bucket): Promise