core: change to @zip.js/zip.js

This commit is contained in:
undefined committed 2025-04-14 16:22:54 +08:00
1 parent d46de63264
commit e31b84d29e
17 files changed
+129 -108

No files matched your search

+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+9 -8
View File
@@ -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 { }
+1 -2
View File
@@ -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"
}
+13 -19
View File
@@ -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;
+2 -1
View File
@@ -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",
+2 -2
View File
@@ -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,
+8 -7
View File
@@ -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`);
}
}
+14 -8
View File
@@ -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 {
+7 -3
View File
@@ -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 = <T = never>(args: {
using?: keyof Context[];
inject?: keyof Context[] | Record<keyof Context, any>;
apply: (ctx: Context, config: T) => Promise<void> | void;
schema?: Schema<T>;
name?: string;
Config?: Schema<T>;
}) => args;
+9 -11
View File
@@ -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 {
+7 -10
View File
@@ -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 {
+7 -10
View File
@@ -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 {
+8 -5
View File
@@ -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) {
+9 -9
View File
@@ -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'],
});
+30 -10
View File
@@ -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<T>(zipOrEntries: ZipReader<T> | 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) {
+1 -1
View File
@@ -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"
},