mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
judge: update limit
This commit is contained in:
1 parent
0481876536
commit
7d1ddff47b
10 files changed
+17
-336
No files matched your search
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hydrooj/hydrojudge",
|
"name": "@hydrooj/hydrojudge",
|
||||||
"bin": "bin/hydrojudge.js",
|
"bin": "bin/hydrojudge.js",
|
||||||
"version": "2.1.30",
|
"version": "2.1.31",
|
||||||
"main": "package.json",
|
"main": "package.json",
|
||||||
"author": "masnn",
|
"author": "masnn",
|
||||||
"repository": "https://github.com/hydro-dev/Hydro.git",
|
"repository": "https://github.com/hydro-dev/Hydro.git",
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ async function read0(folder: string, files: string[], checkFile) {
|
|||||||
count: 0,
|
count: 0,
|
||||||
subtasks: [{
|
subtasks: [{
|
||||||
time_limit_ms: 1000,
|
time_limit_ms: 1000,
|
||||||
memory_limit_mb: 256,
|
memory_limit_mb: 512,
|
||||||
type: 'sum',
|
type: 'sum',
|
||||||
cases: [],
|
cases: [],
|
||||||
score: Math.floor(100 / cases.length),
|
score: Math.floor(100 / cases.length),
|
||||||
@@ -104,7 +104,7 @@ async function read0(folder: string, files: string[], checkFile) {
|
|||||||
if (extra < cases.length) {
|
if (extra < cases.length) {
|
||||||
config.subtasks.push({
|
config.subtasks.push({
|
||||||
time_limit_ms: 1000,
|
time_limit_ms: 1000,
|
||||||
memory_limit_mb: 256,
|
memory_limit_mb: 512,
|
||||||
type: 'sum',
|
type: 'sum',
|
||||||
cases: [],
|
cases: [],
|
||||||
score: Math.floor(100 / cases.length) + 1,
|
score: Math.floor(100 / cases.length) + 1,
|
||||||
@@ -133,7 +133,7 @@ async function read1(folder: string, files: string[], checkFile) {
|
|||||||
if (!subtask[REG.subtask(data)]) {
|
if (!subtask[REG.subtask(data)]) {
|
||||||
subtask[REG.subtask(data)] = [{
|
subtask[REG.subtask(data)] = [{
|
||||||
time_limit_ms: 1000,
|
time_limit_ms: 1000,
|
||||||
memory_limit_mb: 256,
|
memory_limit_mb: 512,
|
||||||
type: 'min',
|
type: 'min',
|
||||||
cases: [c],
|
cases: [c],
|
||||||
}];
|
}];
|
||||||
@@ -276,7 +276,7 @@ function convertIniConfig(ini: string) {
|
|||||||
cases: [{ input: `input/${input.toLowerCase()}`, output: `output/${output.toLowerCase()}` }],
|
cases: [{ input: `input/${input.toLowerCase()}`, output: `output/${output.toLowerCase()}` }],
|
||||||
score: parseInt(score, 10),
|
score: parseInt(score, 10),
|
||||||
time: `${time}s`,
|
time: `${time}s`,
|
||||||
memory: '256m',
|
memory: '512m',
|
||||||
};
|
};
|
||||||
if (!Number.isNaN(parseInt(memory, 10))) cur.memory = `${Math.floor(parseInt(memory, 10) / 1024)}m`;
|
if (!Number.isNaN(parseInt(memory, 10))) cur.memory = `${Math.floor(parseInt(memory, 10) / 1024)}m`;
|
||||||
res.subtasks.push(cur);
|
res.subtasks.push(cur);
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class JudgeTask {
|
|||||||
this.tmpdir = path.resolve(TEMP_DIR, 'tmp', this.host, this.rid);
|
this.tmpdir = path.resolve(TEMP_DIR, 'tmp', this.host, this.rid);
|
||||||
this.clean = [];
|
this.clean = [];
|
||||||
fs.ensureDirSync(this.tmpdir);
|
fs.ensureDirSync(this.tmpdir);
|
||||||
tmpfs.mount(this.tmpdir, '256m');
|
tmpfs.mount(this.tmpdir, '512m');
|
||||||
log.info('Submission: %s/%s/%s pid=%s', this.host, this.domainId, this.rid, this.pid);
|
log.info('Submission: %s/%s/%s pid=%s', this.host, this.domainId, this.rid, this.pid);
|
||||||
try {
|
try {
|
||||||
if (this.type === 'judge') await this.doSubmission();
|
if (this.type === 'judge') await this.doSubmission();
|
||||||
@@ -231,7 +231,7 @@ export default class Hydro {
|
|||||||
fs.ensureDirSync(path.dirname(savePath));
|
fs.ensureDirSync(path.dirname(savePath));
|
||||||
const zip = new AdmZip(tmpFilePath);
|
const zip = new AdmZip(tmpFilePath);
|
||||||
const entries = zip.getEntries();
|
const entries = zip.getEntries();
|
||||||
if (entries.length > 256) throw new FormatError('Too many files');
|
if (entries.length > 512) throw new FormatError('Too many files');
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
zip.extractAllToAsync(savePath, true, (e) => {
|
zip.extractAllToAsync(savePath, true, (e) => {
|
||||||
if (e) reject(e);
|
if (e) reject(e);
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class JudgeTask {
|
|||||||
this.tmpdir = path.resolve(TEMP_DIR, 'tmp', this.host, this.rid);
|
this.tmpdir = path.resolve(TEMP_DIR, 'tmp', this.host, this.rid);
|
||||||
this.clean = [];
|
this.clean = [];
|
||||||
fs.ensureDirSync(this.tmpdir);
|
fs.ensureDirSync(this.tmpdir);
|
||||||
tmpfs.mount(this.tmpdir, '256m');
|
tmpfs.mount(this.tmpdir, '512m');
|
||||||
log.info('Submission: %s/%s/%s pid=%s', this.host, this.domain_id, this.rid, this.pid);
|
log.info('Submission: %s/%s/%s pid=%s', this.host, this.domain_id, this.rid, this.pid);
|
||||||
try {
|
try {
|
||||||
if (this.type === 0) await this.doSubmission();
|
if (this.type === 0) await this.doSubmission();
|
||||||
@@ -256,7 +256,7 @@ export default class VJ4 {
|
|||||||
fs.ensureDirSync(path.dirname(savePath));
|
fs.ensureDirSync(path.dirname(savePath));
|
||||||
const zip = new AdmZip(tmpFilePath);
|
const zip = new AdmZip(tmpFilePath);
|
||||||
const entries = zip.getEntries();
|
const entries = zip.getEntries();
|
||||||
if (entries.length > 256) throw new FormatError('Too many files');
|
if (entries.length > 512) throw new FormatError('Too many files');
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
zip.extractAllToAsync(savePath, true, (e) => {
|
zip.extractAllToAsync(savePath, true, (e) => {
|
||||||
if (e) reject(e);
|
if (e) reject(e);
|
||||||
@@ -286,7 +286,7 @@ export default class VJ4 {
|
|||||||
fs.ensureDirSync(path.dirname(savePath));
|
fs.ensureDirSync(path.dirname(savePath));
|
||||||
const zip = new AdmZip(tmpFilePath);
|
const zip = new AdmZip(tmpFilePath);
|
||||||
const entries = zip.getEntries();
|
const entries = zip.getEntries();
|
||||||
if (entries.length > 256) throw new FormatError('Too many files');
|
if (entries.length > 512) throw new FormatError('Too many files');
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
zip.extractAllToAsync(savePath, true, (e) => {
|
zip.extractAllToAsync(savePath, true, (e) => {
|
||||||
if (e) reject(e);
|
if (e) reject(e);
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export const judge = async (ctx) => {
|
|||||||
copyIn: execute.copyIn,
|
copyIn: execute.copyIn,
|
||||||
copyOut,
|
copyOut,
|
||||||
time_limit_ms: 1000,
|
time_limit_ms: 1000,
|
||||||
memory_limit_mb: 256,
|
memory_limit_mb: 512,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const { code, time_usage_ms, memory_usage_kb } = res;
|
const { code, time_usage_ms, memory_usage_kb } = res;
|
||||||
@@ -90,7 +90,7 @@ export const judge = async (ctx) => {
|
|||||||
if (status === STATUS.STATUS_ACCEPTED) {
|
if (status === STATUS.STATUS_ACCEPTED) {
|
||||||
if (time_usage_ms > 1000) {
|
if (time_usage_ms > 1000) {
|
||||||
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
|
status = STATUS.STATUS_TIME_LIMIT_EXCEEDED;
|
||||||
} else if (memory_usage_kb > 256 * 1024) {
|
} else if (memory_usage_kb > 512 * 1024) {
|
||||||
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
|
status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED;
|
||||||
} else {
|
} else {
|
||||||
[status, , message] = await check({
|
[status, , message] = await check({
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ async function postInit() {
|
|||||||
fs.ensureDirSync(path.dirname(savePath));
|
fs.ensureDirSync(path.dirname(savePath));
|
||||||
const zip = new AdmZip(tmpFilePath);
|
const zip = new AdmZip(tmpFilePath);
|
||||||
const entries = zip.getEntries();
|
const entries = zip.getEntries();
|
||||||
if (entries.length > 256) throw new FormatError('Too many files.');
|
if (entries.length > 512) throw new FormatError('Too many files.');
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
zip.extractAllToAsync(savePath, true, (e) => {
|
zip.extractAllToAsync(savePath, true, (e) => {
|
||||||
if (e) reject(e);
|
if (e) reject(e);
|
||||||
@@ -217,7 +217,7 @@ async function postInit() {
|
|||||||
this.tmpdir = path.resolve(tmpdir(), 'tmp', this.rid);
|
this.tmpdir = path.resolve(tmpdir(), 'tmp', this.rid);
|
||||||
this.clean = [];
|
this.clean = [];
|
||||||
fs.ensureDirSync(this.tmpdir);
|
fs.ensureDirSync(this.tmpdir);
|
||||||
tmpfs.mount(this.tmpdir, '256m');
|
tmpfs.mount(this.tmpdir, '512m');
|
||||||
logger.info(`Submission: ${this.rid}`, { pid: this.pid });
|
logger.info(`Submission: ${this.rid}`, { pid: this.pid });
|
||||||
if (this.config.input) await this.run();
|
if (this.config.input) await this.run();
|
||||||
else if (this.config.hack) await this.hack();
|
else if (this.config.hack) await this.hack();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydrooj",
|
"name": "hydrooj",
|
||||||
"version": "2.14.1",
|
"version": "2.14.2",
|
||||||
"bin": "bin/hydrooj.js",
|
"bin": "bin/hydrooj.js",
|
||||||
"main": "dist/loader.js",
|
"main": "dist/loader.js",
|
||||||
"typings": "dist/loader.d.ts",
|
"typings": "dist/loader.d.ts",
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ export class ProblemPretestHandler extends ProblemDetailHandler {
|
|||||||
{
|
{
|
||||||
input,
|
input,
|
||||||
time: '1s',
|
time: '1s',
|
||||||
memory: '256m',
|
memory: '512m',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const rdoc = await record.get(domainId, rid);
|
const rdoc = await record.get(domainId, rid);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ function convert(ini: string) {
|
|||||||
cases: [{ input: `input/${input}`, output: `output/${output}` }],
|
cases: [{ input: `input/${input}`, output: `output/${output}` }],
|
||||||
score: parseInt(score, 10),
|
score: parseInt(score, 10),
|
||||||
time: `${time}s`,
|
time: `${time}s`,
|
||||||
memory: '256m',
|
memory: '512m',
|
||||||
};
|
};
|
||||||
if (!Number.isNaN(parseInt(memory, 10))) cur.memory = `${Math.floor(parseInt(memory, 10) / 1024)}m`;
|
if (!Number.isNaN(parseInt(memory, 10))) cur.memory = `${Math.floor(parseInt(memory, 10) / 1024)}m`;
|
||||||
res.subtasks.push(cur);
|
res.subtasks.push(cur);
|
||||||
|
|||||||
@@ -1,319 +0,0 @@
|
|||||||
import { URL } from 'url';
|
|
||||||
import { Readable } from 'stream';
|
|
||||||
import { v4 as UUID } from 'uuid';
|
|
||||||
import { Client as MinioClient } from 'minio';
|
|
||||||
|
|
||||||
import { FileEntity } from './file.entity';
|
|
||||||
|
|
||||||
import { FileUploadInfoDto, SignedFileUploadRequestDto } from './dto';
|
|
||||||
|
|
||||||
// 10 minutes upload expire time
|
|
||||||
const FILE_UPLOAD_EXPIRE_TIME = 10 * 60;
|
|
||||||
// 20 minutes download expire time
|
|
||||||
const FILE_DOWNLOAD_EXPIRE_TIME = 20 * 60 * 60;
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
|
|
||||||
function encodeRFC5987ValueChars(str: string) {
|
|
||||||
return (
|
|
||||||
encodeURIComponent(str)
|
|
||||||
// Note that although RFC3986 reserves "!", RFC5987 does not,
|
|
||||||
// so we do not need to escape it
|
|
||||||
.replace(/['()]/g, escape) // i.e., %27 %28 %29
|
|
||||||
.replace(/\*/g, '%2A')
|
|
||||||
// The following are not required for percent-encoding per RFC5987,
|
|
||||||
// so we can allow for a little better readability over the wire: |`^
|
|
||||||
.replace(/%(?:7C|60|5E)/g, unescape)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MinioEndpointConfig {
|
|
||||||
endPoint: string;
|
|
||||||
port: number;
|
|
||||||
useSSL: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMainEndpointUrl(endpoint: string): MinioEndpointConfig {
|
|
||||||
const url = new URL(endpoint);
|
|
||||||
const result: Partial<MinioEndpointConfig> = {};
|
|
||||||
if (url.pathname !== '/') throw new Error('Main MinIO endpoint URL of a sub-directory is not supported.');
|
|
||||||
if (url.username || url.password || url.hash || url.search) {
|
|
||||||
throw new Error('Authorization, search parameters and hash are not supported for main MinIO endpoint URL.');
|
|
||||||
}
|
|
||||||
if (url.protocol === 'http:') result.useSSL = false;
|
|
||||||
else if (url.protocol === 'https:') result.useSSL = true;
|
|
||||||
else {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid protocol "${url.protocol}" for main MinIO endpoint URL. Only HTTP and HTTPS are supported.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
result.endPoint = url.hostname;
|
|
||||||
result.port = url.port ? Number(url.port) : result.useSSL ? 443 : 80;
|
|
||||||
return result as MinioEndpointConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseAlternativeEndpointUrl(endpoint: string): (originalUrl: string) => string {
|
|
||||||
if (!endpoint) return (originalUrl) => originalUrl;
|
|
||||||
const url = new URL(endpoint);
|
|
||||||
if (url.hash || url.search) {
|
|
||||||
throw new Error('Search parameters and hash are not supported for alternative MinIO endpoint URL.');
|
|
||||||
}
|
|
||||||
if (!url.pathname.endsWith('/')) throw new Error("Alternative MinIO endpoint URL's pathname must ends with '/'.");
|
|
||||||
return (originalUrl) => {
|
|
||||||
const parsedOriginUrl = new URL(originalUrl);
|
|
||||||
return new URL(parsedOriginUrl.pathname.slice(1) + parsedOriginUrl.search + parsedOriginUrl.hash, url).toString();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AlternativeUrlFor {
|
|
||||||
User,
|
|
||||||
Judge
|
|
||||||
}
|
|
||||||
|
|
||||||
const minioClient = new MinioClient({
|
|
||||||
...parseMainEndpointUrl(config.endpoint),
|
|
||||||
accessKey:
|
|
||||||
})
|
|
||||||
|
|
||||||
export class FileService {
|
|
||||||
private readonly minioClient: MinioClient;
|
|
||||||
|
|
||||||
private readonly bucket: string;
|
|
||||||
|
|
||||||
private readonly replaceWithAlternativeUrlFor: Record<AlternativeUrlFor, (originalUrl: string) => string>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly connection: Connection,
|
|
||||||
private readonly fileRepository: Repository<FileEntity>,
|
|
||||||
private readonly configService: ConfigService,
|
|
||||||
) {
|
|
||||||
const config = this.configService.config.services.minio;
|
|
||||||
|
|
||||||
this.minioClient = new MinioClient({
|
|
||||||
...parseMainEndpointUrl(config.endpoint),
|
|
||||||
accessKey: config.accessKey,
|
|
||||||
secretKey: config.secretKey,
|
|
||||||
});
|
|
||||||
this.bucket = config.bucket;
|
|
||||||
this.replaceWithAlternativeUrlFor = {
|
|
||||||
[AlternativeUrlFor.User]: parseAlternativeEndpointUrl(config.endpointForUser),
|
|
||||||
[AlternativeUrlFor.Judge]: parseAlternativeEndpointUrl(config.endpointForJudge),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fileExistsInMinio(uuid: string): Promise<boolean> {
|
|
||||||
return new Promise((resolve) =>
|
|
||||||
this.minioClient
|
|
||||||
.statObject(this.bucket, uuid)
|
|
||||||
.then(() => resolve(true))
|
|
||||||
.catch(() => resolve(false)));
|
|
||||||
}
|
|
||||||
|
|
||||||
async uploadFile(uuid: string, streamOrBufferOrFile: string | Buffer | Readable, retryCount = 10): Promise<void> {
|
|
||||||
for (let i = 0; i < retryCount; i++) {
|
|
||||||
try {
|
|
||||||
/* eslint-disable no-await-in-loop */
|
|
||||||
if (typeof streamOrBufferOrFile === 'string') { await this.minioClient.fPutObject(this.bucket, uuid, streamOrBufferOrFile, {}); } else await this.minioClient.putObject(this.bucket, uuid, streamOrBufferOrFile, {});
|
|
||||||
/* eslint-enable no-await-in-loop */
|
|
||||||
} catch (e) {
|
|
||||||
if (i === retryCount - 1) throw e;
|
|
||||||
else {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async onModuleInit(): Promise<void> {
|
|
||||||
let bucketExists: boolean;
|
|
||||||
try {
|
|
||||||
bucketExists = await this.minioClient.bucketExists(this.bucket);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(
|
|
||||||
`Error initializing the MinIO client. Please check your configuration file and MinIO server. ${e}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bucketExists) {
|
|
||||||
throw new Error(
|
|
||||||
`MinIO bucket ${this.bucket} doesn't exist. Please check your configuration file and MinIO server.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process the client's request about uploading a file.
|
|
||||||
*
|
|
||||||
* * If the user has not uploaded the file, return a signed upload info object.
|
|
||||||
* * If the user has uploaded the file, check the file existance, file size and limits.
|
|
||||||
*
|
|
||||||
* @note
|
|
||||||
* The `checkLimit` function may return different result for each call with the same upload info.
|
|
||||||
*
|
|
||||||
* e.g. When a user uploaded other files before upload this file, the quota is enough before uploading but
|
|
||||||
* not enough after uploading.
|
|
||||||
*
|
|
||||||
* If the file is checked to be exceeding the limit after uploaded, it will be deleted.
|
|
||||||
*
|
|
||||||
* @return A `SignedFileUploadRequestDto` to return to the client if the file is not uploaded.
|
|
||||||
* @return `string` error message if client says the file is uploaded but we do not accept it according to some errors.
|
|
||||||
* @return A `FileEntity` if the file is uploaded successfully and saved to the database.
|
|
||||||
*/
|
|
||||||
async processUploadRequest<LimitCheckErrorType extends string>(
|
|
||||||
uploadInfo: FileUploadInfoDto,
|
|
||||||
checkLimit: (size: number) => Promise<LimitCheckErrorType> | LimitCheckErrorType,
|
|
||||||
transactionalEntityManager: EntityManager,
|
|
||||||
): Promise<FileEntity | SignedFileUploadRequestDto | LimitCheckErrorType | 'FILE_UUID_EXISTS' | 'FILE_NOT_UPLOADED'> {
|
|
||||||
const limitCheckError = await checkLimit(uploadInfo.size);
|
|
||||||
if (limitCheckError) {
|
|
||||||
this.deleteUnfinishedUploadedFile(uploadInfo.uuid);
|
|
||||||
return limitCheckError;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uploadInfo.uuid) {
|
|
||||||
// The client says the file is uploaded
|
|
||||||
|
|
||||||
if ((await transactionalEntityManager.count(FileEntity, { uuid: uploadInfo.uuid })) !== 0) return 'FILE_UUID_EXISTS';
|
|
||||||
|
|
||||||
// Check file existance
|
|
||||||
try {
|
|
||||||
await this.minioClient.statObject(this.bucket, uploadInfo.uuid);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message === 'The specified key does not exist.') {
|
|
||||||
return 'FILE_NOT_UPLOADED';
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to the database
|
|
||||||
const file = new FileEntity();
|
|
||||||
file.uuid = uploadInfo.uuid;
|
|
||||||
file.size = uploadInfo.size;
|
|
||||||
file.uploadTime = new Date();
|
|
||||||
|
|
||||||
await transactionalEntityManager.save(FileEntity, file);
|
|
||||||
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
// The client says it want to upload a file for this request
|
|
||||||
|
|
||||||
return await this.signUploadRequest(uploadInfo.size, uploadInfo.size);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign a upload request for given size. The alternative MinIO endpoint for user will be used in the POST URL.
|
|
||||||
*/
|
|
||||||
private async signUploadRequest(minSize?: number, maxSize?: number): Promise<SignedFileUploadRequestDto> {
|
|
||||||
const uuid = UUID();
|
|
||||||
const policy = this.minioClient.newPostPolicy();
|
|
||||||
policy.setBucket(this.bucket);
|
|
||||||
policy.setKey(uuid);
|
|
||||||
policy.setExpires(new Date(Date.now() + FILE_UPLOAD_EXPIRE_TIME * 1000));
|
|
||||||
if (minSize != null || maxSize != null) {
|
|
||||||
policy.setContentLengthRange(minSize || 0, maxSize || 0);
|
|
||||||
}
|
|
||||||
const policyResult = await this.minioClient.presignedPostPolicy(policy);
|
|
||||||
|
|
||||||
return {
|
|
||||||
uuid,
|
|
||||||
method: 'POST',
|
|
||||||
url: this.replaceWithAlternativeUrlFor[AlternativeUrlFor.User](policyResult.postURL),
|
|
||||||
extraFormData: policyResult.formData,
|
|
||||||
fileFieldName: 'file',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return A function to run after transaction, to delete the file(s) actually.
|
|
||||||
*/
|
|
||||||
async deleteFile(uuid: string | string[], transactionalEntityManager: EntityManager): Promise<() => void> {
|
|
||||||
if (typeof uuid === 'string') {
|
|
||||||
await transactionalEntityManager.delete(FileEntity, { uuid });
|
|
||||||
return () =>
|
|
||||||
this.minioClient.removeObject(this.bucket, uuid).catch((e) => {
|
|
||||||
logger.error(`Failed to delete file ${uuid}: ${e}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (uuid.length > 0) {
|
|
||||||
await transactionalEntityManager.delete(FileEntity, { uuid: In(uuid) });
|
|
||||||
return () =>
|
|
||||||
this.minioClient.removeObjects(this.bucket, uuid).catch((e) => {
|
|
||||||
logger.error(`Failed to delete file [${uuid}]: ${e}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
/* do nothing */
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a user-uploaded file before calling finishUpload()
|
|
||||||
*/
|
|
||||||
deleteUnfinishedUploadedFile(uuid: string): void {
|
|
||||||
this.minioClient.removeObject(this.bucket, uuid).catch((e) => {
|
|
||||||
if (e.message === 'The specified key does not exist.') return;
|
|
||||||
logger.error(`Failed to delete unfinished uploaded file ${uuid}: ${e}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getFileSizes(uuids: string[], transcationalEntityManager: EntityManager): Promise<number[]> {
|
|
||||||
if (uuids.length === 0) return [];
|
|
||||||
const uniqueUuids = Array.from(new Set(uuids));
|
|
||||||
const files = await transcationalEntityManager.find(FileEntity, {
|
|
||||||
uuid: In(uniqueUuids),
|
|
||||||
});
|
|
||||||
const map = Object.fromEntries(files.map((file) => [file.uuid, file]));
|
|
||||||
return uuids.map((uuid) => map[uuid].size);
|
|
||||||
}
|
|
||||||
|
|
||||||
async signDownloadLink({
|
|
||||||
uuid,
|
|
||||||
downloadFilename,
|
|
||||||
noExpire,
|
|
||||||
useAlternativeEndpointFor,
|
|
||||||
}: {
|
|
||||||
uuid: string;
|
|
||||||
downloadFilename?: string;
|
|
||||||
noExpire?: boolean;
|
|
||||||
useAlternativeEndpointFor?: AlternativeUrlFor;
|
|
||||||
}): Promise<string> {
|
|
||||||
const url = await this.minioClient.presignedGetObject(
|
|
||||||
this.bucket,
|
|
||||||
uuid,
|
|
||||||
// The maximum expire time is 7 days
|
|
||||||
noExpire ? 24 * 60 * 60 * 7 : FILE_DOWNLOAD_EXPIRE_TIME,
|
|
||||||
!downloadFilename
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
'response-content-disposition': `attachment; filename="${encodeRFC5987ValueChars(downloadFilename)}"`,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (useAlternativeEndpointFor != null) return this.replaceWithAlternativeUrlFor[useAlternativeEndpointFor](url);
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
async runMaintainceTasks(): Promise<void> {
|
|
||||||
// Delete unused files
|
|
||||||
// TODO: Use listObjectsV2 instead, which returns at most 1000 objects in a time
|
|
||||||
const stream = this.minioClient.listObjects(this.bucket);
|
|
||||||
const deleteList: string[] = [];
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const promises: Promise<void>[] = [];
|
|
||||||
stream.on('data', (object) => {
|
|
||||||
promises.push(
|
|
||||||
(async () => {
|
|
||||||
const uuid = object.name;
|
|
||||||
if (!(await this.fileRepository.count({ uuid }))) {
|
|
||||||
deleteList.push(uuid);
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
stream.on('end', () => Promise.all(promises).then(resolve).catch(reject));
|
|
||||||
stream.on('error', reject);
|
|
||||||
});
|
|
||||||
await this.minioClient.removeObjects(this.bucket, deleteList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user