mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 10:15:31 +00:00
199 lines
5 KiB
TypeScript
199 lines
5 KiB
TypeScript
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<ArchiveFileModel[]> {
|
|
const rows = await getArchiveFilesLoader(ctx).load(md5);
|
|
return rows.map((row) => new ArchiveFileModel(ctx, row));
|
|
}
|
|
|
|
static async fromFileMd5(
|
|
ctx: UserContext,
|
|
md5: string
|
|
): Promise<ArchiveFileModel | null> {
|
|
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<Int | null> {
|
|
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<string | null> {
|
|
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<string | null> {
|
|
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<string> {
|
|
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<SkinModel> {
|
|
return SkinModel.fromMd5Assert(this.ctx, this.getMd5());
|
|
}
|
|
|
|
/**
|
|
* The skin in which this file was found
|
|
* @gqlField skin
|
|
*/
|
|
async skin(): Promise<ISkin | null> {
|
|
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<FileInfoModel | null> {
|
|
return FileInfoModel.fromFileMd5(this.ctx, this.getFileMd5());
|
|
}
|
|
|
|
async debug(): Promise<ArchiveFileDebugData> {
|
|
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<ArchiveFileModel | null> {
|
|
return ArchiveFileModel.fromFileMd5(ctx, md5);
|
|
}
|
|
|
|
const getArchiveFilesLoader = ctxWeakMapMemoize<
|
|
DataLoader<string, ArchiveFileRow[]>
|
|
>(
|
|
() =>
|
|
new DataLoader<string, ArchiveFileRow[]>(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<string, ArchiveFileRow>
|
|
>(
|
|
() =>
|
|
new DataLoader<string, ArchiveFileRow>(async (md5s) => {
|
|
const rows = await knex("archive_files")
|
|
.whereIn("file_md5", md5s)
|
|
.select();
|
|
return md5s.map((md5) => rows.find((x) => x.file_md5 === md5));
|
|
})
|
|
);
|