diff --git a/packages/skin-database/api/graphql/resolvers/ClassicSkinResolver.ts b/packages/skin-database/api/graphql/resolvers/ClassicSkinResolver.ts index ae879bd2..56a3dbb2 100644 --- a/packages/skin-database/api/graphql/resolvers/ClassicSkinResolver.ts +++ b/packages/skin-database/api/graphql/resolvers/ClassicSkinResolver.ts @@ -24,11 +24,7 @@ export default class ClassicSkinResolver implements NodeResolver, ISkin { return toId(this.__typename, this.md5()); } async filename(normalize_extension?: boolean): Promise { - const filename = await this._model.getFileName(); - if (normalize_extension) { - return path.parse(filename).name + ".wsz"; - } - return filename; + return await this._model.getFileName(normalize_extension); } museum_url(): string { diff --git a/packages/skin-database/data/SkinModel.ts b/packages/skin-database/data/SkinModel.ts index 4cab1479..d7543c39 100644 --- a/packages/skin-database/data/SkinModel.ts +++ b/packages/skin-database/data/SkinModel.ts @@ -158,7 +158,7 @@ export default class SkinModel { return "UNREVIEWED"; } - async getFileName(): Promise { + async getFileName(normalizeExtension?: boolean): Promise { const files = await this.getFiles(); if (files.length === 0) { throw new Error(`Could not find file for skin with md5 ${this.getMd5()}`); @@ -167,6 +167,9 @@ export default class SkinModel { if (!filename.match(/\.(zip)|(wsz)|(wal)$/i)) { throw new Error("Expected filename to end with zip, wsz or wal."); } + if (normalizeExtension) { + return path.parse(filename).name + ".wsz"; + } return filename; } diff --git a/packages/skin-database/services/internetArchive.ts b/packages/skin-database/services/internetArchive.ts index da89100e..5d483c8d 100644 --- a/packages/skin-database/services/internetArchive.ts +++ b/packages/skin-database/services/internetArchive.ts @@ -1,5 +1,16 @@ import fetch from "node-fetch"; -import { exec } from "../utils"; +import { execFile } from "../utils"; +import path from "path"; + +// Path to the ia command in the virtual environment +const IA_COMMAND = path.join(__dirname, "../.venv/bin/ia"); + +// Environment variables for the virtual environment +const getVenvEnv = () => ({ + ...process.env, + PATH: `${path.join(__dirname, "../.venv/bin")}:${process.env.PATH}`, + VIRTUAL_ENV: path.join(__dirname, "../.venv"), +}); export async function fetchMetadata(identifier: string): Promise { const r = await fetch(`https://archive.org/metadata/${identifier}`); @@ -11,9 +22,8 @@ export async function fetchMetadata(identifier: string): Promise { } export async function fetchTasks(identifier: string): Promise { - const command = `ia tasks ${identifier}`; - const result = await exec(command, { - encoding: "utf8", + const result = await execFile(IA_COMMAND, ['tasks', identifier], { + env: getVenvEnv(), }); return result.stdout .trim() @@ -25,12 +35,47 @@ export async function uploadFile( identifier: string, filepath: string ): Promise { - const command = `ia upload ${identifier} "${filepath}"`; - await exec(command, { encoding: "utf8" }); + await execFile(IA_COMMAND, ['upload', identifier, filepath], { + env: getVenvEnv(), + }); +} + +export async function uploadFiles( + identifier: string, + filepaths: string[], + metadata?: { [key: string]: string } +): Promise { + const args = ['upload', identifier, ...filepaths]; + + if (metadata) { + Object.entries(metadata).forEach(([key, value]) => { + args.push(`--metadata=${key}:${value}`); + }); + } + + await execFile(IA_COMMAND, args, { env: getVenvEnv() }); +} + +export async function uploadFiles( + identifier: string, + filepaths: string[], + metadata?: { [key: string]: string } +): Promise { + const args = ['upload', identifier, ...filepaths]; + + if (metadata) { + Object.entries(metadata).forEach(([key, value]) => { + args.push(`--metadata=${key}:${value}`); + }); + } + + await execFile(IA_COMMAND, args, { env: getVenvEnv() }); } export async function identifierExists(identifier: string): Promise { - const result = await exec(`ia metadata ${identifier}`); + const result = await execFile(IA_COMMAND, ['metadata', identifier], { + env: getVenvEnv(), + }); const data = JSON.parse(result.stdout); return Object.keys(data).length > 0; } @@ -40,7 +85,6 @@ export async function setMetadata( data: { [key: string]: string } ) { const pairs = Object.entries(data).map(([key, value]) => `${key}:${value}`); - const args = pairs.map((pair) => `--modify="${pair}"`); - const command = `ia metadata ${identifier} ${args.join(" ")}`; - await exec(command); + const args = ['metadata', identifier, ...pairs.map((pair) => `--modify=${pair}`)]; + await execFile(IA_COMMAND, args, { env: getVenvEnv() }); } diff --git a/packages/skin-database/tasks/syncToArchive.ts b/packages/skin-database/tasks/syncToArchive.ts index 0498e73d..3d0ddd22 100644 --- a/packages/skin-database/tasks/syncToArchive.ts +++ b/packages/skin-database/tasks/syncToArchive.ts @@ -8,7 +8,7 @@ import SkinModel from "../data/SkinModel"; import * as Parallel from "async-parallel"; import IaItemModel from "../data/IaItemModel"; import DiscordEventHandler from "../api/DiscordEventHandler"; -import { exec } from "../utils"; +import { exec, execFile } from "../utils"; import * as IAService from "../services/internetArchive"; export async function findItemsMissingImages(): Promise { @@ -192,7 +192,7 @@ async function getNewIdentifier(filename: string): Promise { } export async function archive(skin: SkinModel): Promise { - const filename = await skin.getFileName(); + const filename = await skin.getFileName(true); const screenshotFilename = await skin.getScreenshotFileName(); const title = `Winamp Skin: ${filename}`; @@ -207,8 +207,36 @@ export async function archive(skin: SkinModel): Promise { console.log(`Going to try to upload with identifier "${identifier}"...`); - const command = `ia upload ${identifier} "${skinFile}" "${screenshotFile}" --metadata="collection:winampskins" --metadata="skintype:wsz" --metadata="mediatype:software" --metadata="title:${title}"`; - await exec(command, { encoding: "utf8" }); + // Path to the ia command in the virtual environment + const IA_COMMAND = path.join(__dirname, "../.venv/bin/ia"); + + // Environment variables for the virtual environment + const venvEnv = { + ...process.env, + PATH: `${path.join(__dirname, "../.venv/bin")}:${process.env.PATH}`, + VIRTUAL_ENV: path.join(__dirname, "../.venv"), + }; + + const metadata = { + collection: "winampskins", + skintype: "wsz", + mediatype: "software", + title: title, + }; + + // Build arguments array for ia upload command + const args = [ + "upload", + identifier, + skinFile, + screenshotFile, + `--metadata=collection:${metadata.collection}`, + `--metadata=skintype:${metadata.skintype}`, + `--metadata=mediatype:${metadata.mediatype}`, + `--metadata=title:${metadata.title}`, + ]; + + await execFile(IA_COMMAND, args, { env: venvEnv }); await knex("ia_items").insert({ skin_md5: skin.getMd5(), identifier }); return identifier; } diff --git a/packages/skin-database/utils.ts b/packages/skin-database/utils.ts index 2bcf783a..b5d69ede 100644 --- a/packages/skin-database/utils.ts +++ b/packages/skin-database/utils.ts @@ -6,6 +6,7 @@ import child_process from "child_process"; import path from "path"; export const exec = util.promisify(child_process.exec); +export const execFile = util.promisify(child_process.execFile); const temp = _temp.track();