Compare commits

...

1 Commits

Author SHA1 Message Date
Johann150 82105c4026
server: fix drive quota for remote users
This deletes as many files as necessary to ensure the drive quota for
remote users is kept. Previously only one file would have been deleted
for each file added.

Co-authored-by: CGsama <CGsama@outlook.com>
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
2023-01-09 21:27:05 +01:00
1 changed files with 30 additions and 20 deletions

View File

@ -2,9 +2,10 @@ import * as fs from 'node:fs';
import { v4 as uuid } from 'uuid';
import S3 from 'aws-sdk/clients/s3.js';
import { IsNull } from 'typeorm';
import { In, IsNull } from 'typeorm';
import sharp from 'sharp';
import { db } from '@/db/postgre.js';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import { publishMainStream, publishDriveStream } from '@/services/stream.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
@ -290,25 +291,33 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, _type: string
if (result) logger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`);
}
async function deleteOldFile(user: IRemoteUser): Promise<void> {
const q = DriveFiles.createQueryBuilder('file')
.where('file.userId = :userId', { userId: user.id })
.andWhere('NOT file.isLink');
async function expireOldFiles(user: IRemoteUser, driveCapacity: number): Promise<void> {
// Delete as many files as necessary so the total usage is below driveCapacity,
// oldest files first, and exclude avatar and banner.
const exceededFileIds = await db.query('SELECT "id" FROM ('
+ 'SELECT "id", SUM("size") OVER (ORDER BY "createdAt" DESC) AS "total" FROM "drive_file" WHERE "userId" = $1 AND NOT "isLink"'
+ (user.avatarId ? ' AND "id" != $2' : '')
+ (user.bannerId ? ' AND "id" != $3' : '')
+ ') AS "totals" WHERE "total" > $4',
[
user.id,
user.avatarId ?? '',
user.bannerId ?? '',
driveCapacity,
]
);
if (user.avatarId) {
q.andWhere('file.id != :avatarId', { avatarId: user.avatarId });
if (exceededFileIds.length === 0) {
// no files to expire, avatar and banner if present are already the only files
throw new Error('remote user drive quota met by avatar and banner');
}
if (user.bannerId) {
q.andWhere('file.id != :bannerId', { bannerId: user.bannerId });
}
const files = await DriveFiles.findBy({
id: In(exceededFileIds.map(x => x.id)),
});
q.orderBy('file.id', 'ASC');
const oldFile = await q.getOne();
if (oldFile) {
deleteFile(oldFile, true);
for (const file of files) {
deleteFile(file, true);
}
}
@ -373,19 +382,20 @@ export async function addFile({
//#region Check drive usage
if (user && !isLink) {
const usage = await DriveFiles.calcDriveUsageOf(user.id);
const isLocalUser = Users.isLocalUser(user);
const instance = await fetchMeta();
const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb);
const driveCapacity = 1024 * 1024 * (isLocalUser ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb);
logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`);
// If usage limit exceeded
if (usage + info.size > driveCapacity) {
if (Users.isLocalUser(user)) {
if (isLocalUser) {
throw new Error('no-free-space');
} else {
// delete oldest file (excluding banner and avatar)
deleteOldFile(await Users.findOneByOrFail({ id: user.id }) as IRemoteUser);
// delete older files to make space for new file
expireOldFiles(await Users.findOneByOrFail({ id: user.id }) as IRemoteUser, driveCapacity - info.size);
}
}
}