diff --git a/framework/eslint-config/package.json b/framework/eslint-config/package.json index 9d6785e41..e94632a49 100644 --- a/framework/eslint-config/package.json +++ b/framework/eslint-config/package.json @@ -4,7 +4,7 @@ "license": "MIT", "main": "base.mjs", "dependencies": { - "@eslint-react/eslint-plugin": "^1.45.3", + "@eslint-react/eslint-plugin": "^1.47.2", "@eslint/compat": "^1.2.8", "@eslint/eslintrc": "^3.3.1", "@stylistic/eslint-plugin": "^4.2.0", diff --git a/package.json b/package.json index f59eb9268..945116f3c 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "mongodb-memory-server": "10.1.4", "nyc": "^17.1.0", "ora": "^8.2.0", - "oxlint": "^0.16.5", + "oxlint": "^0.16.6", "package-json": "^10.0.1", "semver": "^7.7.1", "simple-git": "^3.27.0", diff --git a/packages/fps-importer/index.ts b/packages/fps-importer/index.ts index 51b88cbdb..05dfce064 100644 --- a/packages/fps-importer/index.ts +++ b/packages/fps-importer/index.ts @@ -1,9 +1,10 @@ /* eslint-disable no-await-in-loop */ +import { Readable } from 'stream'; import decodeHTML from 'decode-html'; import xml2js from 'xml2js'; import { - _, AdmZip, BadRequestError, buildContent, Context, FileTooLargeError, fs, Handler, - PERM, ProblemConfigFile, ProblemModel, ProblemType, Schema, SolutionModel, SystemModel, ValidationError, yaml, + _, BadRequestError, buildContent, Context, FileTooLargeError, fs, Handler, PERM, ProblemConfigFile, + ProblemModel, ProblemType, Schema, SolutionModel, SystemModel, ValidationError, yaml, Zip, } from 'hydrooj'; const knownRemoteMapping = { @@ -93,17 +94,17 @@ class FpsProblemImportHandler extends Handler { } catch (e) { if (e instanceof FileTooLargeError) throw e; console.log(e); - let zip: AdmZip; + const zip = new Zip.ZipReader(Readable.toWeb(fs.createReadStream(file.filepath))); + let entries: Zip.Entry[]; try { - zip = new AdmZip(file.filepath); + entries = await zip.getEntries(); } catch (err) { throw new ValidationError('zip', null, err.message); } - for (const entry of zip.getEntries()) { + for (const entry of entries) { try { - const buf = entry.getData(); - if (buf.byteLength > SystemModel.get('import-fps.limit')) throw new FileTooLargeError(); - const content = buf.toString(); + if (entry.uncompressedSize > SystemModel.get('import-fps.limit')) throw new FileTooLargeError(); + const content = entry.getData(new Zip.TextWriter()); const result = await xml2js.parseStringPromise(content); tasks.push(result); } catch { } diff --git a/packages/hydrojudge/package.json b/packages/hydrojudge/package.json index cc4e8cf5b..daf7cdff9 100644 --- a/packages/hydrojudge/package.json +++ b/packages/hydrojudge/package.json @@ -8,7 +8,7 @@ "dependencies": { "@hydrooj/common": "workspace:^", "@hydrooj/utils": "workspace:^", - "adm-zip": "0.5.16", + "@zip.js/zip.js": "^2.7.60", "cac": "^6.7.14", "p-queue": "^8.1.0", "schemastery": "^3.16.1", @@ -20,7 +20,6 @@ "preferUnplugged": true, "license": "AGPL-3.0-or-later", "devDependencies": { - "@types/adm-zip": "^0.5.7", "@types/shell-quote": "^1.7.5", "@types/ws": "^8.18.1" } diff --git a/packages/hydrojudge/src/hosts/vj4.ts b/packages/hydrojudge/src/hosts/vj4.ts index 64808c0bb..8bdb4ff17 100644 --- a/packages/hydrojudge/src/hosts/vj4.ts +++ b/packages/hydrojudge/src/hosts/vj4.ts @@ -1,11 +1,15 @@ /* eslint-disable no-await-in-loop */ +import { createReadStream } from 'fs'; import path from 'path'; -import AdmZip from 'adm-zip'; +import { Readable } from 'stream'; +import { ZipReader } from '@zip.js/zip.js'; import PQueue from 'p-queue'; import superagent from 'superagent'; import WebSocket from 'ws'; import { JudgeResultBody, parseLang, STATUS } from '@hydrooj/common'; -import { findFileSync, fs, noop } from '@hydrooj/utils'; +import { + extractZip, findFileSync, fs, noop, +} from '@hydrooj/utils'; import { getConfig } from '../config'; import { FormatError, SystemError } from '../error'; import { Session } from '../interface'; @@ -151,16 +155,11 @@ export default class VJ4 implements Session { fs.createWriteStream(tmpFilePath), ); fs.ensureDirSync(path.dirname(savePath)); - const zip = new AdmZip(tmpFilePath); - const entries = zip.getEntries(); + const zip = new ZipReader(Readable.toWeb(createReadStream(tmpFilePath))); + const entries = await zip.getEntries(); if (entries.length > 512) throw new FormatError('Too many files'); - if (Math.sum(entries.map((i) => i.header.size)) > 256 * 1024 * 1024) throw new FormatError('File too large'); - await new Promise((resolve, reject) => { - zip.extractAllToAsync(savePath, true, false, (e) => { - if (e) reject(e); - else resolve(null); - }); - }); + if (Math.sum(entries.map((i) => i.uncompressedSize)) > 256 * 1024 * 1024) throw new FormatError('File too large'); + await extractZip(entries, savePath, { strip: true }); await fs.unlink(tmpFilePath); await this.processData(savePath).catch(noop); } catch (e) { @@ -177,15 +176,10 @@ export default class VJ4 implements Session { const w = fs.createWriteStream(tmpFilePath); await pipeRequest(this.get(`records/${rid}/data`), w); fs.ensureDirSync(path.dirname(savePath)); - const zip = new AdmZip(tmpFilePath); - const entries = zip.getEntries(); + const zip = new ZipReader(Readable.toWeb(createReadStream(tmpFilePath))); + const entries = await zip.getEntries(); if (entries.length > 512) throw new FormatError('Too many files'); - await new Promise((resolve, reject) => { - zip.extractAllToAsync(savePath, true, false, (e) => { - if (e) reject(e); - else resolve(null); - }); - }); + await extractZip(entries, savePath, { strip: true }); await fs.unlink(tmpFilePath); await this.processData(savePath); return savePath; diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 83e536d8a..e9a4640f2 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -24,6 +24,7 @@ "@hydrooj/utils": "workspace:^", "@simplewebauthn/server": "13.1.1", "@undefined-moe/require-resolve-hook": "^1.1.0", + "@zip.js/zip.js": "^2.7.60", "adm-zip": "0.5.16", "cac": "^6.7.14", "cordis": "4.0.0-beta.1", @@ -43,7 +44,7 @@ "mongodb": "^6.15.0", "mongodb-uri": "^0.9.7", "nanoid": "^5.1.5", - "nodemailer": "^6.10.0", + "nodemailer": "^6.10.1", "notp": "^2.0.3", "p-queue": "^8.1.0", "sanitize-filename": "^1.6.3", diff --git a/packages/hydrooj/src/commands/install.ts b/packages/hydrooj/src/commands/install.ts index 93b578773..d034d0d07 100644 --- a/packages/hydrooj/src/commands/install.ts +++ b/packages/hydrooj/src/commands/install.ts @@ -1,7 +1,7 @@ import child from 'child_process'; import os from 'os'; import path from 'path'; -import AdmZip from 'adm-zip'; +import { BlobReader, ZipReader } from '@zip.js/zip.js'; import { CAC } from 'cac'; import fs from 'fs-extra'; import superagent from 'superagent'; @@ -34,7 +34,7 @@ function downloadAndExtractTgz(url: string, dest: string) { } async function downloadAndExtractZip(url: string, dest: string) { const res = await superagent.get(url).responseType('arraybuffer'); - await extractZip(new AdmZip(res.body), dest, true, true); + await extractZip(new ZipReader(new BlobReader(new Blob([res.body]))), dest, { strip: true, overwrite: true }); } const types = { '.tgz': downloadAndExtractTgz, diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 52e6edac2..2ac90bf4c 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -1,10 +1,11 @@ -import AdmZip from 'adm-zip'; +import { Readable } from 'stream'; +import { BlobWriter, TextReader, ZipWriter } from '@zip.js/zip.js'; import { stringify as toCSV } from 'csv-stringify/sync'; import { escapeRegExp, pick } from 'lodash'; import moment from 'moment-timezone'; import { ObjectId } from 'mongodb'; import { - Counter, sortFiles, streamToBuffer, Time, yaml, + Counter, sortFiles, Time, yaml, } from '@hydrooj/utils/lib/utils'; import { Context, Service } from '../context'; import { @@ -399,7 +400,7 @@ export class ContestCodeHandler extends Handler { } } } - const zip = new AdmZip(); + const zip = new ZipWriter(new BlobWriter('application/zip'), { bufferedWrite: true }); const rdocs = await record.getMulti(domainId, { _id: { $in: Array.from(Object.keys(rnames)).map((id) => new ObjectId(id)) }, }).toArray(); @@ -407,15 +408,15 @@ export class ContestCodeHandler extends Handler { if (rdoc.files?.code) { const [id, filename] = rdoc.files?.code?.split('#') || []; if (!id) return; - zip.addFile( + await zip.add( `${rnames[rdoc._id.toHexString()]}.${filename || 'txt'}`, - await streamToBuffer(await storage.get(`submission/${id}`)), + Readable.toWeb(await storage.get(`submission/${id}`)), ); } else if (rdoc.code) { - zip.addFile(`${rnames[rdoc._id.toHexString()]}.${rdoc.lang}`, Buffer.from(rdoc.code)); + await zip.add(`${rnames[rdoc._id.toHexString()]}.${rdoc.lang}`, new TextReader(rdoc.code)); } })); - this.binary(zip.toBuffer(), `${tdoc.title}.zip`); + this.binary(await zip.close(), `${tdoc.title}.zip`); } } diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 0d9ec9244..263acc987 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -1,4 +1,6 @@ -import AdmZip from 'adm-zip'; +import { createReadStream } from 'fs'; +import { PassThrough, Readable, Writable } from 'stream'; +import { Entry, ZipReader } from '@zip.js/zip.js'; import { readFile } from 'fs-extra'; import { escapeRegExp, flattenDeep, intersection, pick, @@ -709,20 +711,24 @@ export class ProblemFilesHandler extends ProblemDetailHandler { filename ||= file.originalFilename || String.random(16); const files = []; if (filename.endsWith('.zip') && type === 'testdata') { - let zip: AdmZip; + const zip = new ZipReader(Readable.toWeb(createReadStream(file.filepath))); + let entries: Entry[]; try { - zip = new AdmZip(file.filepath); + entries = await zip.getEntries(); } catch (e) { throw new ValidationError('zip', null, e.message); } - const entries = zip.getEntries(); for (const entry of entries) { - if (!entry.name || entry.isDirectory) continue; + if (!entry.filename || entry.directory) continue; files.push({ type, - name: sanitize(entry.name), - size: entry.header.size, - data: () => entry.getData(), + name: sanitize(entry.filename), + size: entry.uncompressedSize, + data: () => { + const pass = new PassThrough(); + entry.getData(Writable.toWeb(pass)); + return pass; + }, }); } } else { diff --git a/packages/hydrooj/src/libs.ts b/packages/hydrooj/src/libs.ts index ffd6b34eb..7dbb92373 100644 --- a/packages/hydrooj/src/libs.ts +++ b/packages/hydrooj/src/libs.ts @@ -1,4 +1,5 @@ -import AdmZip from 'adm-zip'; +import * as Zip from '@zip.js/zip.js'; +import _AdmZip from 'adm-zip'; import _ from 'lodash'; import { ObjectId as ObjectID } from 'mongodb'; import Schema from 'schemastery'; @@ -10,11 +11,14 @@ export * from './utils'; export * from './interface'; export * from './typeutils'; export { - Schema, AdmZip, superagent, _, ObjectID, + Schema, superagent, _, ObjectID, Zip, }; +/** @deprecated Use ZipReader/ZipWriter instead */ +export const AdmZip = _AdmZip; export const definePlugin = (args: { - using?: keyof Context[]; + inject?: keyof Context[] | Record; apply: (ctx: Context, config: T) => Promise | void; schema?: Schema; name?: string; + Config?: Schema; }) => args; diff --git a/packages/hydrooj/src/model/problem.ts b/packages/hydrooj/src/model/problem.ts index b4c089bc0..45d9d33a7 100644 --- a/packages/hydrooj/src/model/problem.ts +++ b/packages/hydrooj/src/model/problem.ts @@ -2,14 +2,16 @@ import child from 'child_process'; import os from 'os'; import path from 'path'; -import AdmZip from 'adm-zip'; +import { Readable } from 'stream'; +import { Entry, ZipReader } from '@zip.js/zip.js'; import fs from 'fs-extra'; import yaml from 'js-yaml'; import { keyBy, pick } from 'lodash'; import { Filter, ObjectId } from 'mongodb'; -import type { Readable } from 'stream'; import { ProblemConfigFile, ProblemType } from '@hydrooj/common'; -import { Logger, size, streamToBuffer } from '@hydrooj/utils/lib/utils'; +import { + extractZip, Logger, size, streamToBuffer, +} from '@hydrooj/utils/lib/utils'; import { Context } from '../context'; import { FileUploadError, ProblemNotFoundError, ValidationError } from '../error'; import type { @@ -456,18 +458,14 @@ export class ProblemModel { try { if (filepath.endsWith('.zip')) { tmpdir = path.join(os.tmpdir(), 'hydro', `${Math.random()}.import`); - let zip: AdmZip; + const zip = new ZipReader(Readable.toWeb(fs.createReadStream(filepath))); + let entries: Entry[]; try { - zip = new AdmZip(filepath); + entries = await zip.getEntries(); } catch (e) { throw new ValidationError('zip', null, e.message); } - await new Promise((resolve, reject) => { - zip.extractAllToAsync(tmpdir, true, false, (err) => { - if (err) reject(err); - resolve(null); - }); - }); + await extractZip(entries, tmpdir); } else if (fs.statSync(filepath).isDirectory()) { tmpdir = filepath; } else { diff --git a/packages/import-hoj/index.ts b/packages/import-hoj/index.ts index f6bd8d010..4d7a5e30f 100644 --- a/packages/import-hoj/index.ts +++ b/packages/import-hoj/index.ts @@ -1,9 +1,10 @@ /* eslint-disable no-await-in-loop */ import os from 'os'; import path from 'path'; +import { Readable } from 'stream'; import { - AdmZip, buildContent, Context, fs, Handler, PERM, - ProblemConfigFile, ProblemModel, ValidationError, yaml, + buildContent, Context, extractZip, fs, Handler, PERM, + ProblemConfigFile, ProblemModel, ValidationError, yaml, Zip, } from 'hydrooj'; const tmpdir = path.join(os.tmpdir(), 'hydro', 'import-hoj'); @@ -11,15 +12,11 @@ fs.ensureDirSync(tmpdir); class ImportHojHandler extends Handler { async fromFile(domainId: string, zipfile: string) { - let zip: AdmZip; - try { - zip = new AdmZip(zipfile); - } catch (e) { - throw new ValidationError('zip', null, e.message); - } + const zip = new Zip.ZipReader(Readable.toWeb(fs.createReadStream(zipfile))); const tmp = path.resolve(tmpdir, String.random(32)); - await new Promise((resolve, reject) => { - zip.extractAllToAsync(tmp, true, false, (err) => (err ? reject(err) : resolve(null))); + await extractZip(zip, tmp, { + strip: true, + parseError: (e) => new ValidationError('zip', null, e.message), }); let cnt = 0; try { diff --git a/packages/import-qduoj/index.ts b/packages/import-qduoj/index.ts index ff6036213..6ace605d9 100644 --- a/packages/import-qduoj/index.ts +++ b/packages/import-qduoj/index.ts @@ -1,9 +1,10 @@ /* eslint-disable no-await-in-loop */ import os from 'os'; import path from 'path'; +import { Readable } from 'stream'; import { - AdmZip, buildContent, Context, FileTooLargeError, fs, Handler, PERM, - ProblemConfigFile, ProblemModel, Schema, ValidationError, yaml, + buildContent, Context, extractZip, FileTooLargeError, fs, Handler, PERM, + ProblemConfigFile, ProblemModel, Schema, ValidationError, yaml, Zip, } from 'hydrooj'; const tmpdir = path.join(os.tmpdir(), 'hydro', 'import-qduoj'); @@ -44,15 +45,11 @@ const ProblemSchema = Schema.object({ class ImportQduojHandler extends Handler { async fromFile(domainId: string, zipfile: string) { - let zip: AdmZip; - try { - zip = new AdmZip(zipfile); - } catch (e) { - throw new ValidationError('zip', null, e.message); - } + const zip = new Zip.ZipReader(Readable.toWeb(fs.createReadStream(zipfile))); const tmp = path.resolve(tmpdir, String.random(32)); - await new Promise((resolve, reject) => { - zip.extractAllToAsync(tmp, true, false, (err) => (err ? reject(err) : resolve(null))); + await extractZip(zip, tmp, { + strip: true, + parseError: (e) => new ValidationError('zip', null, e.message), }); let cnt = 0; try { diff --git a/packages/migrate/scripts/universaloj.ts b/packages/migrate/scripts/universaloj.ts index 547ef996c..e9f48cdeb 100644 --- a/packages/migrate/scripts/universaloj.ts +++ b/packages/migrate/scripts/universaloj.ts @@ -2,9 +2,9 @@ import mariadb from 'mariadb'; import xml2js from 'xml2js'; import { - AdmZip, ContestModel, DomainModel, fs, MessageModel, moment, + ContestModel, DomainModel, fs, MessageModel, moment, noop, NotFoundError, ObjectId, postJudge, ProblemConfigFile, ProblemModel, RecordDoc, RecordModel, - STATUS, SubtaskType, SystemModel, Time, UserModel, ValidationError, yaml, + STATUS, SubtaskType, SystemModel, Time, UserModel, ValidationError, yaml, Zip, } from 'hydrooj'; const statusMap = { Accepted: STATUS.STATUS_ACCEPTED, @@ -376,13 +376,16 @@ export async function run({ }; const content = JSON.parse(rdoc.content); try { - let zip: AdmZip; + const zip = new Zip.ZipReader( + new Zip.BlobReader(new Blob([await fs.readFile(`${dataDir}/opt/uoj/web/app/storage${content.file_name}`)])), + ); + let entries: Zip.Entry[]; try { - zip = new AdmZip(await fs.readFile(`${dataDir}/opt/uoj/web/app/storage${content.file_name}`)); + entries = await zip.getEntries(); } catch (e) { throw new ValidationError('zip', null, e.message); } - data.code = zip.getEntries().find((i) => i.name.endsWith('answer.code')).getData().toString(); + data.code = await entries.find((i) => i.filename.endsWith('answer.code'))?.getData(new Zip.TextWriter()) || ''; } catch { /* ignore no code */ } const result = JSON.parse(Buffer.from(rdoc.result, 'base64').toString('utf8')); if (result.error) { diff --git a/packages/onsite-toolkit/index.ts b/packages/onsite-toolkit/index.ts index 44e298887..7706914e1 100644 --- a/packages/onsite-toolkit/index.ts +++ b/packages/onsite-toolkit/index.ts @@ -1,9 +1,9 @@ /* eslint-disable max-len */ import moment from 'moment'; import { - AdmZip, avatar, ContestModel, ContestNotEndedError, Context, db, findFileSync, + avatar, ContestModel, ContestNotEndedError, Context, db, findFileSync, ForbiddenError, fs, ObjectId, parseTimeMS, PERM, ProblemConfig, ProblemModel, - STATUS, STATUS_SHORT_TEXTS, STATUS_TEXTS, Time, UserModel, + STATUS, STATUS_SHORT_TEXTS, STATUS_TEXTS, Time, UserModel, Zip, } from 'hydrooj'; import { ResolverInput } from './interface'; @@ -248,19 +248,19 @@ export function apply(ctx: Context) { finalized: moment().format('YYYY-MM-DDTHH:mm:ss.SSS+08:00'), }), ]; - const zip = new AdmZip(); - zip.addFile('event-feed.ndjson', Buffer.from(eventfeed.concat(submissions).concat(endState).map((i) => JSON.stringify(i)).join('\n'))); - zip.addFile('contest/logo.png', fs.readFileSync(findFileSync('@hydrooj/onsite-toolkit/public/logo.png'))); + const zip = new Zip.ZipWriter(new Zip.BlobWriter()); + await zip.add('event-feed.ndjson', new Zip.TextReader(eventfeed.concat(submissions).concat(endState).map((i) => JSON.stringify(i)).join('\n'))); + await zip.add('contest/logo.png', new Zip.BlobReader(new Blob([fs.readFileSync(findFileSync('@hydrooj/onsite-toolkit/public/logo.png'))]))); for (const i of ['teams', 'organizations']) { - zip.addFile(`${i}/`, Buffer.alloc(0)); + zip.add(`${i}/`, null, { directory: true }); } for (const i of teams) { - zip.addFile(`teams/${i.team_id}/photo.download.txt`, Buffer.from(i.avatar)); + zip.add(`teams/${i.team_id}/photo.download.txt`, new Zip.TextReader(i.avatar)); } for (const i of organizations) { - zip.addFile(`organizations/${orgId[i]}/photo.download.txt`, Buffer.from(avatar(teams.find((j) => j.organization === i)?.avatar || 'no photo, find and download it yourself'))); + zip.add(`organizations/${orgId[i]}/photo.download.txt`, new Zip.TextReader(avatar(teams.find((j) => j.organization === i)?.avatar || 'no photo, find and download it yourself'))); } - this.binary(zip.toBuffer(), `contest-${tdoc._id}-cdp.zip`); + this.binary(await zip.close(), `contest-${tdoc._id}-cdp.zip`); }, supportedRules: ['acm'], }); diff --git a/packages/utils/lib/utils.ts b/packages/utils/lib/utils.ts index d2045d28a..c74126083 100644 --- a/packages/utils/lib/utils.ts +++ b/packages/utils/lib/utils.ts @@ -1,9 +1,9 @@ import crypto from 'crypto'; import os from 'os'; import path from 'path'; -import { Duplex, PassThrough } from 'stream'; +import { Duplex, PassThrough, Writable } from 'stream'; import { inspect } from 'util'; -import type AdmZip from 'adm-zip'; +import type { Entry, ZipReader } from '@zip.js/zip.js'; import fs from 'fs-extra'; import moment, { isMoment, Moment } from 'moment-timezone'; import { ObjectId } from 'mongodb'; @@ -319,22 +319,42 @@ export function sanitizePath(pathname: string) { return parts.join(path.sep); } +export interface ExtractZipConfig { + overwrite?: boolean; + strip?: boolean; + signal?: AbortSignal; + parseError?: (err: Error) => Error; +} + /* eslint-disable no-await-in-loop */ -export async function extractZip(zip: AdmZip, dest: string, overwrite = false, strip = false) { - const entries = zip.getEntries(); - const shouldStrip = strip ? entries.every((i) => i.entryName.startsWith(entries[0].entryName)) : false; +export async function extractZip(zipOrEntries: ZipReader | Entry[], dest: string, config: ExtractZipConfig = {}) { + const { overwrite = false, strip = false, signal } = config; + let entries: Entry[]; + if (Array.isArray(zipOrEntries)) entries = zipOrEntries; + else { + try { + entries = await zipOrEntries.getEntries(); + } catch (e) { + if (config.parseError) throw config.parseError(e); + throw e; + } + } + const shouldStrip = strip ? entries.every((i) => i.filename.startsWith(entries[0].filename)) : false; for (const entry of entries) { - const name = shouldStrip ? entry.entryName.substring(entries[0].entryName.length) : entry.entryName; + const name = shouldStrip ? entry.filename.substring(entries[0].filename.length) : entry.filename; const d = sanitize(dest, canonical(name)); - if (entry.isDirectory) { + if (entry.directory) { await fs.mkdir(d, { recursive: true }); continue; } - const content = entry.getData(); + if (fs.existsSync(d) && !overwrite) continue; + const content = await entry.getData(Writable.toWeb(fs.createWriteStream(d)), { signal }); if (!content) throw new Error('CANT_EXTRACT_FILE'); - if (!fs.existsSync(d) || overwrite) await fs.writeFile(d, content); - await fs.utimes(d, entry.header.time, entry.header.time); + await fs.utimes(d, entry.lastModDate, entry.lastModDate); } + return { + [Symbol.asyncDispose]: () => fs.remove(dest), + }; } export async function pipeRequest(req: superagent.Request, w: fs.WriteStream, timeout?: number, name?: string) { diff --git a/packages/vjudge/package.json b/packages/vjudge/package.json index a75586bf3..3202bc2ad 100644 --- a/packages/vjudge/package.json +++ b/packages/vjudge/package.json @@ -9,7 +9,7 @@ "preferUnplugged": true, "dependencies": { "@hydrooj/utils": "workspace:^", - "jsdom": "^26.0.0", + "jsdom": "^26.1.0", "superagent-charset": "^1.2.0", "superagent-proxy": "^3.0.0" },