webamp/packages/skin-database/addSkin.ts
Lorenzo Stanco 162025f8a0
Webamp optionally fully contained into a DOM element (#1338)
* Webamp optionally fully contained into a DOM element

* Fix spelling

* Reenable updating search index for uploads

* Replace contained flag on renderWhenReady with a new method. Add docs

---------

Co-authored-by: Jordan Eldredge <jordan@jordaneldredge.com>
2025-12-31 14:59:14 -08:00

139 lines
3.5 KiB
TypeScript

import * as Skins from "./data/skins";
import fs from "fs";
import md5Buffer from "md5";
import * as S3 from "./s3";
import Shooter from "./shooter";
import _temp from "temp";
import { SkinType } from "./types";
import SkinModel from "./data/SkinModel";
import UserContext from "./data/UserContext";
import JSZip from "jszip";
import { setHashesForSkin } from "./skinHash";
// TODO Move this into the function so that we clean up on each run?
const temp = _temp.track();
// TODO
// Extract the readme
// Extract the emails
// Upload to Internet Archive
// Store the Internet Archive item name
// Construct IA Webamp UR
export type Result =
| { md5: string; status: "FOUND" }
| { md5: string; status: "ADDED"; skinType: SkinType };
export async function addSkinFromBuffer(
buffer: Buffer,
filePath: string,
uploader: string
): Promise<Result> {
const ctx = new UserContext();
const md5 = md5Buffer(buffer);
if (await SkinModel.exists(ctx, md5)) {
console.log("Skin found.");
return { md5, status: "FOUND" };
}
// Note: This will thrown on invalid skins.
console.log("Getting zip...");
const zip = await JSZip.loadAsync(buffer);
console.log("Getting skin type...");
const skinType = await getSkinType(zip);
switch (skinType) {
case "CLASSIC":
console.log("Adding classic skin...");
return addClassicSkinFromBuffer(ctx, buffer, md5, filePath, uploader);
case "MODERN":
return addModernSkinFromBuffer(ctx, buffer, md5, filePath, uploader);
}
}
async function addModernSkinFromBuffer(
ctx: UserContext,
buffer: Buffer,
md5: string,
filePath: string,
uploader: string
): Promise<Result> {
console.log("Write temporarty file.");
const tempFile = temp.path({ suffix: ".wal" });
fs.writeFileSync(tempFile, new Uint8Array(buffer));
console.log("Put skin to S3.");
await S3.putSkin(md5, buffer, "wal");
console.log("Add skin to DB");
await Skins.addSkin({
ctx,
md5,
filePath,
uploader,
modern: true,
});
return { md5, status: "ADDED", skinType: "MODERN" };
}
async function addClassicSkinFromBuffer(
ctx: UserContext,
buffer: Buffer,
md5: string,
filePath: string,
uploader: string
): Promise<Result> {
const tempFile = temp.path({ suffix: ".wsz" });
fs.writeFileSync(tempFile, new Uint8Array(buffer));
const tempScreenshotPath = temp.path({ suffix: ".png" });
const logLines: string[] = [];
const logger = (message: string) => logLines.push(message);
await Shooter.withShooter(
(shooter) =>
shooter.takeScreenshot(tempFile, tempScreenshotPath, {
minify: true,
md5,
}),
logger
);
try {
await S3.putScreenshot(md5, fs.readFileSync(tempScreenshotPath));
} catch (e) {
console.error("Screenshot log lines:", logLines);
throw e;
}
await S3.putSkin(md5, buffer, "wsz");
await Skins.addSkin({
ctx,
md5,
filePath,
uploader,
modern: false,
});
const skin = await SkinModel.fromMd5Assert(ctx, md5);
await setHashesForSkin(skin);
// Disable while we figure out our quota
await Skins.updateSearchIndex(ctx, md5);
return { md5, status: "ADDED", skinType: "CLASSIC" };
}
export async function getSkinType(zip: JSZip): Promise<SkinType> {
const classic = zip.file(/main\.bmp$/i).length > 0;
const modern = zip.file(/skin\.xml$/i).length > 0;
if (classic && modern) {
throw new Error("Skin is both modern and classic.");
}
if (classic) {
return "CLASSIC";
}
if (modern) {
return "MODERN";
}
throw new Error("Not a skin");
}