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