Implement HTML-based downloading

This commit is contained in:
HoshinoKoji 2024-10-01 17:42:21 +08:00
parent 885434b87e
commit 1e48f6ead6
3 changed files with 80 additions and 15 deletions

View File

@ -1,27 +1,39 @@
import { getHashString } from "./utils";
export async function downloadDispatcher(request, env, ctx) { export async function downloadDispatcher(request, env, ctx) {
const url = new URL(request.url); const url = new URL(request.url);
const uri = url.pathname; const uri = url.pathname;
const bucket = env.EXP_DATA; const bucket = env.EXP_DATA;
const signatureBase = env.SIGNATURE_BASE;
const adminSecret = env.ADMIN_SECRET; const adminSecret = env.ADMIN_SECRET;
if (request.headers.get('x-access-token') !== adminSecret)
return new Response(null, { status: 401, statusText: 'Unauthorized' });
try {
const key = uri.slice(10); // '/download/'.length const key = uri.slice(10); // '/download/'.length
const signature = url.searchParams.get('signature');
const timestamp = url.searchParams.get('timestamp');
if (!signature && request.headers.get('x-access-token') !== adminSecret)
return new Response(null, { status: 403, statusText: 'Forbidden' });
else {
const hash = await getHashString(`${key}${timestamp}${signatureBase}`, 'SHA-1');
if (signature !== hash)
return new Response(null, { status: 403, statusText: 'Forbidden' });
}
try {
const object = await bucket.get(key); const object = await bucket.get(key);
if (object === null) if (object === null)
return new Response(null, { status: 404, statusText: 'Not Found' }); return new Response(null, { status: 404, statusText: 'Not Found' });
const { body, etag, httpMetadata, customMetadata } = object; const { body, etag, httpMetadata, customMetadata } = object;
return new Response(body, { headers: { return new Response(body, { headers: {
etag: etag, etag,
'content-type': httpMetadata.contentType, 'content-type': httpMetadata.contentType,
'content-length': body.length, 'content-length': body.length,
'x-metadata': JSON.stringify(customMetadata), 'x-metadata': JSON.stringify(customMetadata),
}}); }});
} catch (e) { return new Response(null, { status: 400 }); } } catch (e) { return new Response(null, { status: 500 }); }
} }
export default downloadDispatcher; export default downloadDispatcher;

View File

@ -1,17 +1,29 @@
import { getHashString, adaptiveFilesize } from "./utils";
export async function listDispatcher(request, env, ctx) { export async function listDispatcher(request, env, ctx) {
const url = new URL(request.url);
const headers = request.headers;
const bucket = env.EXP_DATA; const bucket = env.EXP_DATA;
const signatureBase = env.SIGNATURE_BASE;
const adminSecret = env.ADMIN_SECRET; const adminSecret = env.ADMIN_SECRET;
const badRequest = new Response(null, { status: 400 }); const badRequest = new Response(null, { status: 400 });
if (request.headers.get('x-access-token') !== adminSecret) const token = url.searchParams.get('accessToken') || headers.get('x-access-token');
if (token !== adminSecret)
return new Response(null, { status: 401, statusText: 'Unauthorized' }); return new Response(null, { status: 401, statusText: 'Unauthorized' });
const resType = url.searchParams.get('resType') || headers.get('x-res-type') || 'html';
if (!['json', 'html'].includes(resType))
return badRequest;
try { try {
const type = request.headers.get('x-list-type') || 'prefix'; const type = url.searchParams.get('type') || headers.get('x-type') || 'prefix';
let pattern = request.headers.get('x-pattern') || ''; let pattern = url.searchParams.get('pattern') || headers.get('x-pattern') || '';
let objects = []; let objects = [];
let truncated = true; let truncated = true;
let cursor = ""; let cursor;
switch (type) { switch (type) {
case 'prefix': case 'prefix':
while (truncated) { while (truncated) {
@ -37,12 +49,47 @@ export async function listDispatcher(request, env, ctx) {
return badRequest; return badRequest;
} }
const res = JSON.stringify(objects, ['key', 'etag', 'size', 'uploaded']); const timestamp = Date.now();
return new Response(res, { status: 200 }); const baseUrl = `${url.protocol}//${url.hostname}:${url.port}`;
} catch (e) { const res = await Promise.all(objects.map(async object => {
console.error(e); const { key, etag, size, uploaded } = object;
return badRequest; const hash = await getHashString(`${key}${timestamp}${signatureBase}`, 'SHA-1');
return { key, etag, size, uploaded,
presignedUrl: `${baseUrl}/download/${key}?signature=${hash}&timestamp=${timestamp}` };
}));
console.log(objects[0]);
if (resType === 'json')
return new Response(JSON.stringify(res), { status: 200 });
else if (resType === 'html') {
// Generate a table of links with URLs, sizes, and upload dates
const body = res.map(obj => `
<tr>
<td><a href="${obj.presignedUrl}">${obj.key}</a></td>
<td>${adaptiveFilesize(obj.size)}</td>
<td>${obj.uploaded.toISOString()}</td>
</tr>
`).join('');
return new Response(`
<html>
<head>
<title>Object List</title>
<meta charset="utf-8">
</head>
<body>
<table>
<tr>
<th>Key</th>
<th>Size</th>
<th>Uploaded</th>
</tr>
${body}
</table>
</body>
</html>
`, { status: 200, headers: { 'content-type': 'text/html' } });
} }
} catch (e) { console.log(e); return badRequest; }
} }
export default listDispatcher; export default listDispatcher;

View File

@ -5,3 +5,9 @@ export async function getHashString(string, method = 'SHA-256') {
const hashArray = Array.from(new Uint8Array(hash)); const hashArray = Array.from(new Uint8Array(hash));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
} }
export function adaptiveFilesize(bytes, digits = 2) {
if (bytes < 1024) return `${bytes} B`;
else if (bytes < 1048576) return `${(bytes / 1024).toFixed(digits)} KB`;
else return `${(bytes / 1048576).toFixed(digits)} MB`;
}