import UserContext, { ctxWeakMapMemoize } from "./UserContext"; import { ArchiveFileRow } from "../types"; import DataLoader from "dataloader"; import { knex } from "../db"; import SkinModel from "./SkinModel"; import FileInfoModel from "./FileInfoModel"; import { ISkin } from "../api/graphql/resolvers/CommonSkinResolver"; import SkinResolver from "../api/graphql/resolvers/SkinResolver"; import { Int } from "grats"; export type ArchiveFileDebugData = { row: ArchiveFileRow; }; /** * A file found within a Winamp Skin's .wsz archive * @gqlType ArchiveFile */ export default class ArchiveFileModel { constructor(readonly ctx: UserContext, readonly row: ArchiveFileRow) {} static async fromMd5( ctx: UserContext, md5: string ): Promise { const rows = await getArchiveFilesLoader(ctx).load(md5); return rows.map((row) => new ArchiveFileModel(ctx, row)); } static async fromFileMd5( ctx: UserContext, md5: string ): Promise { const row = await getArchiveFilesByFileMd5Loader(ctx).load(md5); return row == null ? null : new ArchiveFileModel(ctx, row); } /** * Md5 of the _skin_ * * **Note:** This is not the md5 of the file itself. Consider renaming this to * `getSkinMd5` */ getMd5(): string { return this.row.skin_md5; } /** * The md5 hash of the file within the archive * @gqlField file_md5 */ getFileMd5(): string { return this.row.file_md5; } /** * Filename of the file within the archive * @gqlField filename */ getFileName(): string { return this.row.file_name; } getFileDate(): Date { return new Date(this.row.file_date); } /** * The date on the file inside the archive. Given in simplified extended ISO * format (ISO 8601). * @gqlField date */ getIsoDate(): string { return this.getFileDate().toISOString(); } /** * The uncompressed size of the file in bytes. * * **Note:** Will be `null` for directories * @gqlField size */ async getFileSize(): Promise { const info = await this._getFileInfo(); if (info == null) { return null; } return info.getFileSize(); } /** * The content of the file, if it's a text file * @gqlField text_content */ async getTextContent(): Promise { const info = await this._getFileInfo(); if (info == null) { return null; } return info.getTextContent(); } /** * Is the file a directory? * @gqlField is_directory */ getIsDirectory(): boolean { return Boolean(this.row.is_directory); } /** * A URL to download the file. **Note:** This is powered by a little * serverless Cloudflare function which tries to exctact the file on the fly. * It may not work for all files. * @gqlField url */ async getUrl(): Promise { if (this.getIsDirectory()) { return null; } const ext = await this.skinExt(); const filename = encodeURIComponent(this.getFileName()); return `https://zip-worker.jordan1320.workers.dev/zip/${this.getMd5()}.${ext}/${filename}`; } async skinExt(): Promise { const skin = await this.getSkin(); const type = skin.getSkinType(); switch (type) { case "CLASSIC": return "wsz"; case "MODERN": return "wal"; default: throw new Error(`Unexpected skin type: "${type}".`); } } async getSkin(): Promise { return SkinModel.fromMd5Assert(this.ctx, this.getMd5()); } /** * The skin in which this file was found * @gqlField skin */ async skin(): Promise { const model = await SkinModel.fromMd5Assert(this.ctx, this.getMd5()); return SkinResolver.fromModel(model); } // Let's try to keep this as an implementation detail async _getFileInfo(): Promise { return FileInfoModel.fromFileMd5(this.ctx, this.getFileMd5()); } async debug(): Promise { return { row: this.row, }; } } /** * Fetch archive file by it's MD5 hash * * Get information about a file found within a skin's wsz/wal/zip archive. * @gqlQueryField */ export async function fetch_archive_file_by_md5( md5: string, ctx: UserContext ): Promise { return ArchiveFileModel.fromFileMd5(ctx, md5); } const getArchiveFilesLoader = ctxWeakMapMemoize< DataLoader >( () => new DataLoader(async (md5s) => { const rows = await knex("archive_files") .whereIn("skin_md5", md5s) .select(); return md5s.map((md5) => rows.filter((x) => x.skin_md5 === md5)); }) ); const getArchiveFilesByFileMd5Loader = ctxWeakMapMemoize< DataLoader >( () => new DataLoader(async (md5s) => { const rows = await knex("archive_files") .whereIn("file_md5", md5s) .select(); return md5s.map((md5) => rows.find((x) => x.file_md5 === md5)); }) );