mirror of
https://github.com/hyperknot/openfreemap.git
synced 2026-05-21 22:12:15 +00:00
work
This commit is contained in:
43
scripts/cloudflare/r2-dir-list/src/index.ts
Normal file
43
scripts/cloudflare/r2-dir-list/src/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Env } from './types';
|
||||
import { renderTemplFull } from './render';
|
||||
import { getSiteConfig } from './config';
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
const originResponse = await fetch(request);
|
||||
// if status is not 404 or request path not end with '/', return origin response
|
||||
if ((originResponse.status !== 404) || (originResponse.url.slice(-1) !== '/')) {
|
||||
return originResponse;
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
const domain = url.hostname;
|
||||
const path = url.pathname;
|
||||
// remove the leading '/'
|
||||
const objectKey = path.slice(1);
|
||||
const siteConfig = getSiteConfig(env, domain);
|
||||
if (!siteConfig) {
|
||||
// TODO: Should send a email to notify the admin
|
||||
return originResponse;
|
||||
}
|
||||
const bucket = siteConfig.bucket;
|
||||
const index = await bucket.list({
|
||||
// TODO: Should manage limit here, but since I won't have more than 1000 files in a single folder, I just ignore it
|
||||
prefix: objectKey,
|
||||
delimiter: '/',
|
||||
include: ['httpMetadata', 'customMetadata']
|
||||
});
|
||||
// if no object found, return origin response
|
||||
if (index.objects.length === 0 && index.delimitedPrefixes.length === 0) {
|
||||
return originResponse;
|
||||
}
|
||||
return new Response(
|
||||
renderTemplFull(index.objects, index.delimitedPrefixes, path, siteConfig),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
},
|
||||
status: 200,
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
197
scripts/cloudflare/r2-dir-list/src/render.ts
Normal file
197
scripts/cloudflare/r2-dir-list/src/render.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { svgs, cssStyle, defaultFavicon } from './static';
|
||||
import { SiteConfig } from './types';
|
||||
|
||||
export var renderTemplFull = (files: R2Object[], folders: string[], path: string, config: SiteConfig) => {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="icon" href="${config.favicon??defaultFavicon}" type="image/png">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${renderTemplTitle(config.name, path)}</title>
|
||||
${cssStyle}
|
||||
</head>
|
||||
<body>
|
||||
${svgs}
|
||||
<header>
|
||||
<h1>
|
||||
|
||||
<a href="/">${config.name}</a> /
|
||||
<!-- breadcrumbs start -->${renderTemplBreadcrumbs(path)}
|
||||
</h1>
|
||||
</header>
|
||||
<main>
|
||||
<div class="listing">
|
||||
<table aria-describedby="summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="hideable"></th>
|
||||
<th class="name">Name</th>
|
||||
<th class="description">Description</th>
|
||||
<th class="size">Size</th>
|
||||
<th class="date hideable">Modified</th>
|
||||
<th class="hideable"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${(path==='/')?'':renderGoUp(path)}
|
||||
<!-- folders start -->${renderTemplFolders(folders, config)}
|
||||
<!-- files start -->${renderTemplFiles(files, config)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
${generateFooter(config, path)}
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
var renderGoUp = (path: string) => {
|
||||
if(path !== "") {
|
||||
return `
|
||||
<tr>
|
||||
<td class="hideable"></td>
|
||||
<td class="goup">
|
||||
<a href="..">
|
||||
Go up
|
||||
</a>
|
||||
</td>
|
||||
<td class="description">—</td>
|
||||
<td class="size">—</td>
|
||||
<td class="date hideable">—</td>
|
||||
<td class="hideable"></td>
|
||||
</tr>`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
var renderTemplTitle = (siteTitle:string, path: string) => {
|
||||
if(path === "/") {
|
||||
return siteTitle
|
||||
}
|
||||
path = path.slice(0, -1);
|
||||
return `${siteTitle} | ${cleanTitle(path)}`;
|
||||
}
|
||||
|
||||
var cleanTitle = (path: string) => {
|
||||
var parts = path.split("/")
|
||||
// remove the empty strings
|
||||
parts = parts.filter((part) => part !== "")
|
||||
return parts[parts.length-1]
|
||||
}
|
||||
|
||||
var renderTemplBreadcrumbs = (path: string) => {
|
||||
const parts = path.split('/');
|
||||
var output = '';
|
||||
var currentPath = '/';
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
if (parts[i] === '') continue;
|
||||
currentPath += parts[i] + '/';
|
||||
output += `<a href="${currentPath}">${parts[i]}</a> / `;
|
||||
}
|
||||
console.log(output);
|
||||
return output;
|
||||
}
|
||||
|
||||
var renderTemplFolders = (folders: string[], siteConfig: SiteConfig) => {
|
||||
if(typeof folders === 'undefined') return '';
|
||||
var output = '';
|
||||
for (var i = 0; i < folders.length; i++) {
|
||||
output += `<tr class="file ">
|
||||
<td class="hideable"></td>
|
||||
<td class="name"><a href="/${folders[i]}"><svg width="1.5em" height="1em" version="1.1" viewBox="0 0 317 259"><use xlink:href="#folder"></use></svg><span class="name">${cleanFolderName(folders[i])}</span></a></td>
|
||||
<td class="description">${findDesp(siteConfig, '/'+folders[i].slice(0, -1), true)??"—"}</td>
|
||||
<td class="size">—</td>
|
||||
<td class="date hideable">—</td>
|
||||
<td class="hideable"></td>
|
||||
</tr>`;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
var renderTemplFiles = (files: R2Object[], siteConfig: SiteConfig) => {
|
||||
if(typeof files === 'undefined') return '';
|
||||
var output = '';
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
output += `<tr class="file ">
|
||||
<td class="hideable"></td>
|
||||
<td class="name"><a href="/${files[i].key}"><svg width="1.5em" height="1em" version="1.1" viewBox="0 0 265 323"><use xlink:href="#file"></use></svg><span class="name">${cleanFileName(files[i].key)}</span></a></td>
|
||||
<td class="description">${findDesp(siteConfig, '/'+files[i].key, true)??"—"}</td>
|
||||
<td class="size">${humanFileSize(files[i].size)}</td>
|
||||
<td class="date hideable"><time datetime="${files[i].uploaded.toUTCString()}">${files[i].uploaded.toJSON()}</time></td>
|
||||
<td class="hideable"></td>
|
||||
</tr>`;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
var cleanFileName = (name: string) => {
|
||||
return name.split("/").slice(-1).pop()!
|
||||
}
|
||||
|
||||
var cleanFolderName = (name: string) => {
|
||||
return name.slice(0, -1).split("/").slice(-1).pop()!
|
||||
}
|
||||
|
||||
// taken from https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-string
|
||||
var humanFileSize = (bytes: number, si=false, dp=1) => {
|
||||
const thresh = si ? 1000 : 1024;
|
||||
|
||||
if (Math.abs(bytes) < thresh) {
|
||||
return bytes + ' B';
|
||||
}
|
||||
|
||||
const units = si
|
||||
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
let u = -1;
|
||||
const r = 10**dp;
|
||||
|
||||
do {
|
||||
bytes /= thresh;
|
||||
++u;
|
||||
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
|
||||
|
||||
|
||||
return bytes.toFixed(dp) + ' ' + units[u];
|
||||
}
|
||||
|
||||
function findDesp(siteConfig: SiteConfig, path: string, exact: boolean): string | undefined {
|
||||
if (exact) {
|
||||
return siteConfig.desp[path];
|
||||
}
|
||||
const keys = Object.keys(siteConfig.desp);
|
||||
// find the longest match
|
||||
let longestMatch = '/';
|
||||
for (const key of keys) {
|
||||
if (path.startsWith(key) && key.length > longestMatch.length) {
|
||||
longestMatch = key;
|
||||
}
|
||||
}
|
||||
const desp = siteConfig.desp[longestMatch];
|
||||
return desp;
|
||||
}
|
||||
|
||||
function generateFooter(siteConfig: SiteConfig, path: string): string {
|
||||
/// Footer includes:
|
||||
/// - desp of current path, use the most specific entry
|
||||
/// - legal info, if any
|
||||
/// - reference to gitea download site code, as the inspiration of this project
|
||||
|
||||
let contents:string[] = [];
|
||||
const desp = findDesp(siteConfig, path, false);
|
||||
if (desp) {
|
||||
contents.push(`<p>${desp}</p>`);
|
||||
}
|
||||
if (siteConfig.legalInfo) {
|
||||
contents.push(`<p>${siteConfig.legalInfo}</p>`);
|
||||
}
|
||||
if (siteConfig.showPoweredBy) {
|
||||
contents.push(`<p>Powered by <a href="https://github.com/cmj2002/r2-dir-list">r2-dir-list</a></p>`);
|
||||
}
|
||||
return contents.join('');
|
||||
}
|
||||
206
scripts/cloudflare/r2-dir-list/src/static.ts
Normal file
206
scripts/cloudflare/r2-dir-list/src/static.ts
Normal file
File diff suppressed because one or more lines are too long
14
scripts/cloudflare/r2-dir-list/src/types.ts
Normal file
14
scripts/cloudflare/r2-dir-list/src/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export type Env = {
|
||||
[K in `BUCKET_${string}`]: R2Bucket;
|
||||
};
|
||||
|
||||
export interface SiteConfig {
|
||||
name: string;
|
||||
bucket: R2Bucket;
|
||||
desp: {
|
||||
[path: string]: string;
|
||||
};
|
||||
legalInfo?: string;
|
||||
showPoweredBy?: boolean;
|
||||
favicon?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user