diff --git a/packages/hydrooj/src/model/storage.ts b/packages/hydrooj/src/model/storage.ts index 61625ddf8..dfa8afd1a 100644 --- a/packages/hydrooj/src/model/storage.ts +++ b/packages/hydrooj/src/model/storage.ts @@ -4,6 +4,7 @@ import moment from 'moment-timezone'; import { nanoid } from 'nanoid'; import type { Readable } from 'stream'; import { Context } from '../context'; +import { FileNode } from '../interface'; import mime from '../lib/mime'; import db from '../service/db'; import storage from '../service/storage'; @@ -49,25 +50,40 @@ export class StorageModel { ); } + private static async _swapId(fileA: FileNode, fileB: FileNode) { + // try to swap the two file with same content to make sure the first file is no longer being referenced + // Example + // _id: A, path: C, meta: R + // _id: B, path: D, link: A, meta: P + // and we want to delete path=B, the result should be + // _id: A, path: D, meta: P + // _id: B, path: C, link: A, meta: R + await Promise.all([ + StorageModel.coll.updateOne({ _id: fileA._id }, { $set: omit(fileB, ['_id', 'link']) }), + StorageModel.coll.updateOne({ _id: fileB._id }, { $set: omit(fileA, ['_id', 'link']) }), + ]); + } + static async del(path: string[], operator = 1) { if (!path.length) return; - const affected = await StorageModel.coll.find({ path: { $in: path } }).toArray(); - if (!affected.length) return; - const linked = await StorageModel.coll.find({ link: { $in: affected.map((i) => i._id) }, path: { $nin: path } }).toArray(); - const processedIds = []; - for (const i of linked || []) { - if (processedIds.includes(i.link)) continue; - const current = affected.find((t) => t._id === i.link); // to be deleted - // eslint-disable-next-line no-await-in-loop - await Promise.all([ - StorageModel.coll.updateOne({ _id: current._id }, { $set: omit(i, ['_id']) }), - StorageModel.coll.updateOne({ _id: i._id }, { $set: omit(current, ['_id']), $unset: { link: '' } }), - ]); - processedIds.push(i.link); + // files pending to be deleted + const pendingDelete = await StorageModel.coll.find({ path: { $in: path }, autoDelete: null }).toArray(); + if (!pendingDelete.length) return; + // all files not going to be deleted but affected (referencing to the files to be deleted) + const linked = await StorageModel.coll.find({ link: { $in: pendingDelete.map((i) => i._id) }, path: { $nin: path } }).toArray(); + const fileIds = []; + for (const i of pendingDelete) { + const affected = linked.filter((t) => t.link === i._id); + if (!affected.length) { + fileIds.push(i._id); + continue; + } + await StorageModel._swapId(i, affected[0]); // eslint-disable-line no-await-in-loop + fileIds.push(affected[0]._id); // we already swapped the two files, so we need to delete the other one } const autoDelete = moment().add(7, 'day').toDate(); await StorageModel.coll.updateMany( - { path: { $in: path }, autoDelete: null }, + { _id: { $in: fileIds } }, { $set: { autoDelete }, $push: { operator } }, ); }