#!/usr/bin/env 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];
}
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}
File Name |
File Size |
Date |
${rows.map(([link, name, size, date]) => `
${name} |
${size ? humanFileSize(size) : "-"} |
${date?.toLocaleString() ?? "-"} |
`).join("")}
`);
}
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();
}