#!/usr/bin/env -S npx ts-node import { S3Client, ListObjectsV2Command, PutObjectCommand, _Object } from "@aws-sdk/client-s3"; const HIDDEN_FILES = [ "/styles.css", "/logo.svg", ".DS_Store", "index.html", "/fonts/", "/nginx-theme/", ".~tmp~/", "msi/", ]; const Bucket = "packages-element-io"; if (!process.env.CF_R2_ACCESS_KEY_ID || !process.env.CF_R2_TOKEN || !process.env.CF_R2_S3_API) { console.error("Missing environment variables `CF_R2_ACCESS_KEY_ID`, `CF_R2_TOKEN`, `CF_R2_S3_API`"); process.exit(1); } const client = new S3Client({ region: "auto", endpoint: process.env.CF_R2_S3_API, credentials: { accessKeyId: process.env.CF_R2_ACCESS_KEY_ID, secretAccessKey: process.env.CF_R2_TOKEN, }, }); const templateLayout = (content: string) => ` packages.element.io

Browse files & directories.

${content}
`; /** * Format bytes as human-readable text. * https://stackoverflow.com/a/14919494 * * @param bytes Number of bytes. * @param si True to use metric (SI) units, aka powers of 1000. False to use * binary (IEC), aka powers of 1024. * @param dp Number of decimal places to display. * * @return Formatted string. */ function 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]; } const dateTimeOptions: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", day: "2-digit", hour: "2-digit", minute: "numeric", }; function indexLayout(prefix: string, files: _Object[], dirs: string[]): string { const rows: [link: string, name: string, size?: number, date?: Date][] = []; if (prefix) { rows.push(["../index.html", "Parent directory/"]); } for (const dir of dirs) { if (HIDDEN_FILES.includes(`${prefix}/${dir}/`) || HIDDEN_FILES.includes(`${dir}/`)) continue; rows.push([`${dir}/index.html`, dir]); } for (const file of files) { if (!file.Key || HIDDEN_FILES.includes(`/${file.Key}`) || HIDDEN_FILES.includes(file.Key.slice(file.Key.lastIndexOf("/") + 1)) ) { continue; } const name = file.Key.slice(prefix.length); rows.push([name, name, file.Size, file.LastModified]); } return templateLayout(`
/${prefix}
${rows.map(([link, name, size, date]) => ``).join("")}
File Name File Size Date
${size ? humanFileSize(size) : "-"} ${date?.toLocaleString("en-GB", dateTimeOptions) ?? "-"}
`); } async function generateIndex(Prefix: string): Promise<{ files: _Object[]; dirs: string[]; }> { console.info(`Generating index for prefix '${Prefix}'`); const command = new ListObjectsV2Command({ Bucket, Delimiter: "/", Prefix, }); const listResponse = await client.send(command); const files = listResponse.Contents ?? []; const dirs = listResponse.CommonPrefixes ?.map(p => p.Prefix?.slice(Prefix.length).split("/", 2)[0]) .filter(Boolean) as string[] ?? []; const Body = indexLayout(Prefix, files, dirs); await client.send(new PutObjectCommand({ Body, Bucket, ContentType: "text/html", Key: Prefix + "index.html", })); return { files, dirs }; } async function generateIndexRecursive(Prefix = ""): Promise { const { dirs } = await generateIndex(Prefix); for (const dir of dirs) { await generateIndexRecursive(Prefix + dir + "/"); } } async function generateIndexList(prefixes: string[]): Promise { for (const prefix of prefixes) { await generateIndex(prefix); } } const args = process.argv.slice(2); if (args.length) { generateIndexList(args); } else { generateIndexRecursive(); }