You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
756 lines
21 KiB
756 lines
21 KiB
import type { WriteStream } from 'fs';
|
|
import fs from 'fs';
|
|
import { unlink, rename, writeFile } from 'fs/promises';
|
|
import stream from 'stream';
|
|
import type * as http from 'http';
|
|
import type * as https from 'https';
|
|
import { Buffer } from 'buffer';
|
|
import URL from 'url';
|
|
|
|
import { Meteor } from 'meteor/meteor';
|
|
import type { WritableStreamBuffer } from 'stream-buffers';
|
|
import streamBuffers from 'stream-buffers';
|
|
import sharp from 'sharp';
|
|
import { Cookies } from 'meteor/ostrio:cookies';
|
|
import { Match } from 'meteor/check';
|
|
import { Users, Avatars, UserDataFiles, Uploads, Settings, Subscriptions, Messages, Rooms } from '@rocket.chat/models';
|
|
import filesize from 'filesize';
|
|
import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions';
|
|
import { hashLoginToken } from '@rocket.chat/account-utils';
|
|
import type { IUpload } from '@rocket.chat/core-typings';
|
|
import type { NextFunction } from 'connect';
|
|
import type { OptionalId } from 'mongodb';
|
|
|
|
import { UploadFS } from '../../../../server/ufs';
|
|
import { settings } from '../../../settings/server';
|
|
import { mime } from '../../../utils/lib/mimeTypes';
|
|
import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom';
|
|
import { fileUploadIsValidContentType } from '../../../utils/server/restrictions';
|
|
import { isValidJWT, generateJWT } from '../../../utils/server/lib/JWTHelper';
|
|
import { AppEvents, Apps } from '../../../../ee/server/apps';
|
|
import { streamToBuffer } from './streamToBuffer';
|
|
import { SystemLogger } from '../../../../server/lib/logger/system';
|
|
import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator';
|
|
import type { Store, StoreOptions } from '../../../../server/ufs/ufs-store';
|
|
import { ufsComplete } from '../../../../server/ufs/ufs-methods';
|
|
import { i18n } from '../../../../server/lib/i18n';
|
|
|
|
const cookie = new Cookies();
|
|
let maxFileSize = 0;
|
|
|
|
settings.watch('FileUpload_MaxFileSize', async function (value: string) {
|
|
try {
|
|
maxFileSize = parseInt(value);
|
|
} catch (e) {
|
|
maxFileSize = (await Settings.findOneById('FileUpload_MaxFileSize'))?.packageValue as number;
|
|
}
|
|
});
|
|
|
|
const handlers: Record<string, FileUploadClass> = {};
|
|
|
|
const defaults: Record<string, () => Partial<StoreOptions>> = {
|
|
Uploads() {
|
|
return {
|
|
collection: Uploads,
|
|
filter: new UploadFS.Filter({
|
|
onCheck: FileUpload.validateFileUpload,
|
|
}),
|
|
getPath(file: IUpload) {
|
|
return `${settings.get('uniqueID')}/uploads/${file.rid}/${file.userId}/${file._id}`;
|
|
},
|
|
onValidate: FileUpload.uploadsOnValidate,
|
|
async onRead(_fileId: string, file: IUpload, req: http.IncomingMessage, res: http.ServerResponse) {
|
|
// Deprecated: Remove support to usf path
|
|
if (!(await FileUpload.requestCanAccessFiles(req, file))) {
|
|
res.writeHead(403);
|
|
return false;
|
|
}
|
|
|
|
res.setHeader('content-disposition', `attachment; filename="${encodeURIComponent(file.name || '')}"`);
|
|
return true;
|
|
},
|
|
};
|
|
},
|
|
|
|
Avatars() {
|
|
return {
|
|
collection: Avatars,
|
|
filter: new UploadFS.Filter({
|
|
onCheck: FileUpload.validateAvatarUpload,
|
|
}),
|
|
getPath(file: IUpload) {
|
|
const avatarFile = file.rid ? `room-${file.rid}` : file.userId;
|
|
return `${settings.get('uniqueID')}/avatars/${avatarFile}`;
|
|
},
|
|
onValidate: FileUpload.avatarsOnValidate,
|
|
onFinishUpload: FileUpload.avatarsOnFinishUpload,
|
|
};
|
|
},
|
|
|
|
UserDataFiles() {
|
|
return {
|
|
collection: UserDataFiles,
|
|
getPath(file: IUpload) {
|
|
return `${settings.get('uniqueID')}/uploads/userData/${file.userId}`;
|
|
},
|
|
onValidate: FileUpload.uploadsOnValidate,
|
|
async onRead(_fileId: string, file: IUpload, req: http.IncomingMessage, res: http.ServerResponse) {
|
|
if (!(await FileUpload.requestCanAccessFiles(req))) {
|
|
res.writeHead(403);
|
|
return false;
|
|
}
|
|
|
|
res.setHeader('content-disposition', `attachment; filename="${encodeURIComponent(file.name || '')}"`);
|
|
return true;
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
export const FileUpload = {
|
|
handlers,
|
|
|
|
getPath(path = '') {
|
|
return `/file-upload/${path}`;
|
|
},
|
|
|
|
configureUploadsStore(store: string, name: string, options: any) {
|
|
const type = name.split(':').pop();
|
|
if (!type || !(type in FileUpload.defaults)) {
|
|
throw new Error('Store type undefined');
|
|
}
|
|
const stores = UploadFS.getStores();
|
|
delete stores[name];
|
|
|
|
return new UploadFS.store[store](
|
|
Object.assign(
|
|
{
|
|
name,
|
|
},
|
|
options,
|
|
FileUpload.defaults[type](),
|
|
),
|
|
);
|
|
},
|
|
|
|
async validateFileUpload(file: IUpload, content?: Buffer) {
|
|
if (!Match.test(file.rid, String)) {
|
|
return false;
|
|
}
|
|
|
|
// livechat users can upload files but they don't have an userId
|
|
const user = (file.userId && (await Users.findOne(file.userId))) || undefined;
|
|
|
|
const room = await Rooms.findOneById(file.rid);
|
|
if (!room) {
|
|
return false;
|
|
}
|
|
const directMessageAllowed = settings.get('FileUpload_Enabled_Direct');
|
|
const fileUploadAllowed = settings.get('FileUpload_Enabled');
|
|
if (user?.type !== 'app' && (await canAccessRoomAsync(room, user, file)) !== true) {
|
|
return false;
|
|
}
|
|
const language = user?.language || 'en';
|
|
if (!fileUploadAllowed) {
|
|
const reason = i18n.t('FileUpload_Disabled', { lng: language });
|
|
throw new Meteor.Error('error-file-upload-disabled', reason);
|
|
}
|
|
|
|
if (!directMessageAllowed && room.t === 'd') {
|
|
const reason = i18n.t('File_not_allowed_direct_messages', { lng: language });
|
|
throw new Meteor.Error('error-direct-message-file-upload-not-allowed', reason);
|
|
}
|
|
|
|
// -1 maxFileSize means there is no limit
|
|
if (maxFileSize > -1 && (file.size || 0) > maxFileSize) {
|
|
const reason = i18n.t('File_exceeds_allowed_size_of_bytes', {
|
|
size: filesize(maxFileSize),
|
|
lng: language,
|
|
});
|
|
throw new Meteor.Error('error-file-too-large', reason);
|
|
}
|
|
|
|
if (!fileUploadIsValidContentType(file.type as string, '')) {
|
|
const reason = i18n.t('File_type_is_not_accepted', { lng: language });
|
|
throw new Meteor.Error('error-invalid-file-type', reason);
|
|
}
|
|
|
|
// App IPreFileUpload event hook
|
|
try {
|
|
await Apps.triggerEvent(AppEvents.IPreFileUpload, { file, content: content || Buffer.from([]) });
|
|
} catch (error: any) {
|
|
if (error.name === AppsEngineException.name) {
|
|
throw new Meteor.Error('error-app-prevented', error.message);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
async validateAvatarUpload(file: IUpload) {
|
|
if (!Match.test(file.rid, String) && !Match.test(file.userId, String)) {
|
|
return false;
|
|
}
|
|
|
|
const user = file.uid ? await Users.findOne(file.uid, { projection: { language: 1 } }) : null;
|
|
const language = user?.language || 'en';
|
|
|
|
// accept only images
|
|
if (!/^image\//.test(file.type || '')) {
|
|
const reason = i18n.t('File_type_is_not_accepted', { lng: language });
|
|
throw new Meteor.Error('error-invalid-file-type', reason);
|
|
}
|
|
|
|
// -1 maxFileSize means there is no limit
|
|
if (maxFileSize > -1 && (file.size || 0) > maxFileSize) {
|
|
const reason = i18n.t('File_exceeds_allowed_size_of_bytes', {
|
|
size: filesize(maxFileSize),
|
|
lng: language,
|
|
});
|
|
throw new Meteor.Error('error-file-too-large', reason);
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
defaults,
|
|
|
|
async avatarsOnValidate(this: Store, file: IUpload) {
|
|
if (settings.get('Accounts_AvatarResize') !== true) {
|
|
return;
|
|
}
|
|
|
|
const tempFilePath = UploadFS.getTempFilePath(file._id);
|
|
|
|
const height = settings.get('Accounts_AvatarSize') as number;
|
|
const width = height as number;
|
|
|
|
const s = sharp(tempFilePath);
|
|
if (settings.get('FileUpload_RotateImages') === true) {
|
|
s.rotate();
|
|
}
|
|
|
|
const metadata = await s.metadata();
|
|
// if (!metadata) {
|
|
// metadata = {};
|
|
// }
|
|
|
|
const { data, info } = await s
|
|
.resize({
|
|
width,
|
|
height,
|
|
fit: metadata.hasAlpha ? sharp.fit.contain : sharp.fit.cover,
|
|
background: { r: 255, g: 255, b: 255, alpha: metadata.hasAlpha ? 0 : 1 },
|
|
})
|
|
// Use buffer to get the result in memory then replace the existing file
|
|
// There is no option to override a file using this library
|
|
//
|
|
// BY THE SHARP DOCUMENTATION:
|
|
// toBuffer: Write output to a Buffer. JPEG, PNG, WebP, TIFF and RAW output are supported.
|
|
// By default, the format will match the input image, except GIF and SVG input which become PNG output.
|
|
.toBuffer({ resolveWithObject: true });
|
|
|
|
try {
|
|
await writeFile(tempFilePath, data);
|
|
} catch (err: any) {
|
|
SystemLogger.error(err);
|
|
}
|
|
|
|
await this.getCollection().updateOne(
|
|
{ _id: file._id },
|
|
{
|
|
$set: {
|
|
size: info.size,
|
|
...(['gif', 'svg'].includes(metadata.format || '') ? { type: 'image/png' } : {}),
|
|
},
|
|
},
|
|
);
|
|
},
|
|
|
|
async resizeImagePreview(fileParam: IUpload) {
|
|
let file = await Uploads.findOneById(fileParam._id);
|
|
if (!file) {
|
|
return;
|
|
}
|
|
file = FileUpload.addExtensionTo(file);
|
|
const image = await FileUpload.getStore('Uploads')._store.getReadStream(file._id, file);
|
|
|
|
const transformer = sharp().resize({ width: 32, height: 32, fit: 'inside' }).jpeg().blur();
|
|
const result = transformer.toBuffer().then((out) => out.toString('base64'));
|
|
image.pipe(transformer);
|
|
return result;
|
|
},
|
|
|
|
async extractMetadata(file: IUpload) {
|
|
return sharp(await FileUpload.getBuffer(file)).metadata();
|
|
},
|
|
|
|
async createImageThumbnail(fileParam: IUpload) {
|
|
if (!settings.get('Message_Attachments_Thumbnails_Enabled')) {
|
|
return;
|
|
}
|
|
|
|
const width = settings.get('Message_Attachments_Thumbnails_Width') as number;
|
|
const height = settings.get('Message_Attachments_Thumbnails_Height') as number;
|
|
|
|
if (fileParam.identify?.size && fileParam.identify.size.height < height && fileParam.identify?.size.width < width) {
|
|
return;
|
|
}
|
|
|
|
let file = await Uploads.findOneById(fileParam._id);
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
file = FileUpload.addExtensionTo(file);
|
|
const store = FileUpload.getStore('Uploads');
|
|
const image = await store._store.getReadStream(file._id, file);
|
|
|
|
const transformer = sharp().resize({ width, height, fit: 'inside' });
|
|
|
|
const result = transformer.toBuffer({ resolveWithObject: true }).then(({ data, info: { width, height } }) => ({ data, width, height }));
|
|
image.pipe(transformer);
|
|
|
|
return result;
|
|
},
|
|
|
|
async uploadImageThumbnail(file: IUpload, buffer: Buffer, rid: string, userId: string) {
|
|
const store = FileUpload.getStore('Uploads');
|
|
const details = {
|
|
name: `thumb-${file.name}`,
|
|
size: buffer.length,
|
|
type: file.type,
|
|
originalFileId: file._id,
|
|
typeGroup: 'thumb',
|
|
uploadedAt: new Date(),
|
|
_updatedAt: new Date(),
|
|
rid,
|
|
userId,
|
|
};
|
|
|
|
return store.insert(details, buffer);
|
|
},
|
|
|
|
async uploadsOnValidate(this: Store, file: IUpload) {
|
|
if (!file.type || !/^image\/((x-windows-)?bmp|p?jpeg|png|gif|webp)$/.test(file.type)) {
|
|
return;
|
|
}
|
|
|
|
const tmpFile = UploadFS.getTempFilePath(file._id);
|
|
|
|
const s = sharp(tmpFile);
|
|
const metadata = await s.metadata();
|
|
// if (err != null) {
|
|
// SystemLogger.error(err);
|
|
// return fut.return();
|
|
// }
|
|
|
|
const rotated = typeof metadata.orientation !== 'undefined' && metadata.orientation !== 1;
|
|
const width = rotated ? metadata.height : metadata.width;
|
|
const height = rotated ? metadata.width : metadata.height;
|
|
|
|
const identify = {
|
|
format: metadata.format,
|
|
size:
|
|
width != null && height != null
|
|
? {
|
|
width,
|
|
height,
|
|
}
|
|
: undefined,
|
|
};
|
|
|
|
const reorientation = async () => {
|
|
if (!rotated || settings.get('FileUpload_RotateImages') !== true) {
|
|
return;
|
|
}
|
|
|
|
await s.rotate().toFile(`${tmpFile}.tmp`);
|
|
|
|
await unlink(tmpFile);
|
|
|
|
await rename(`${tmpFile}.tmp`, tmpFile);
|
|
// SystemLogger.error(err);
|
|
};
|
|
|
|
await reorientation();
|
|
|
|
const { size } = await fs.lstatSync(tmpFile);
|
|
await this.getCollection().updateOne(
|
|
{ _id: file._id },
|
|
{
|
|
$set: { size, identify },
|
|
},
|
|
);
|
|
},
|
|
|
|
async avatarsOnFinishUpload(file: IUpload) {
|
|
if (file.rid) {
|
|
return;
|
|
}
|
|
|
|
if (!file.userId) {
|
|
throw new Meteor.Error('error-not-allowed', 'Change avatar is not allowed');
|
|
}
|
|
|
|
// update file record to match user's username
|
|
const user = await Users.findOneById(file.userId);
|
|
if (!user?.username) {
|
|
throw new Meteor.Error('error-not-allowed', 'Change avatar is not allowed');
|
|
}
|
|
const oldAvatar = await Avatars.findOneByName(user.username);
|
|
if (oldAvatar) {
|
|
await Avatars.deleteFile(oldAvatar._id);
|
|
}
|
|
await Avatars.updateFileNameById(file._id, user.username);
|
|
// console.log('upload finished ->', file);
|
|
},
|
|
|
|
async requestCanAccessFiles({ headers = {}, url }: http.IncomingMessage, file?: IUpload) {
|
|
if (!url || !settings.get('FileUpload_ProtectFiles')) {
|
|
return true;
|
|
}
|
|
|
|
const { query } = URL.parse(url, true);
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
let { rc_uid, rc_token, rc_rid, rc_room_type } = query as Record<string, string | undefined>;
|
|
const { token } = query;
|
|
|
|
if (!rc_uid && headers.cookie) {
|
|
rc_uid = cookie.get('rc_uid', headers.cookie);
|
|
rc_token = cookie.get('rc_token', headers.cookie);
|
|
rc_rid = cookie.get('rc_rid', headers.cookie);
|
|
rc_room_type = cookie.get('rc_room_type', headers.cookie);
|
|
}
|
|
|
|
const isAuthorizedByRoom = async () =>
|
|
rc_room_type &&
|
|
roomCoordinator
|
|
.getRoomDirectives(rc_room_type)
|
|
.canAccessUploadedFile({ rc_uid: rc_uid || '', rc_rid: rc_rid || '', rc_token: rc_token || '' });
|
|
|
|
const isAuthorizedByJWT = () =>
|
|
settings.get('FileUpload_Enable_json_web_token_for_files') &&
|
|
token &&
|
|
isValidJWT(token as string, settings.get('FileUpload_json_web_token_secret_for_files'));
|
|
|
|
if ((await isAuthorizedByRoom()) || isAuthorizedByJWT()) {
|
|
return true;
|
|
}
|
|
|
|
const uid = rc_uid || (headers['x-user-id'] as string);
|
|
const authToken = rc_token || (headers['x-auth-token'] as string);
|
|
|
|
const user = uid && authToken && (await Users.findOneByIdAndLoginToken(uid, hashLoginToken(authToken), { projection: { _id: 1 } }));
|
|
|
|
if (!user) {
|
|
return false;
|
|
}
|
|
|
|
if (!settings.get('FileUpload_Restrict_to_room_members') || !file?.rid) {
|
|
return true;
|
|
}
|
|
|
|
const subscription = await Subscriptions.findOneByRoomIdAndUserId(file.rid, user._id, { projection: { _id: 1 } });
|
|
|
|
if (subscription) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
addExtensionTo(file: IUpload) {
|
|
if (mime.lookup(file.name || '') === file.type) {
|
|
return file;
|
|
}
|
|
|
|
// This file type can be pretty much anything, so it's better if we don't mess with the file extension
|
|
if (file.type !== 'application/octet-stream') {
|
|
const ext = mime.extension(file.type || '');
|
|
if (ext && new RegExp(`\\.${ext}$`, 'i').test(file.name || '') === false) {
|
|
file.name = `${file.name}.${ext}`;
|
|
}
|
|
}
|
|
|
|
return file;
|
|
},
|
|
|
|
getStore(modelName: string) {
|
|
const storageType = settings.get('FileUpload_Storage_Type');
|
|
const handlerName = `${storageType}:${modelName}`;
|
|
|
|
return this.getStoreByName(handlerName);
|
|
},
|
|
|
|
getStoreByName(handlerName?: string) {
|
|
if (!handlerName) {
|
|
SystemLogger.error(`Empty Upload handler does not exists`);
|
|
throw new Error(`Empty Upload handler does not exists`);
|
|
}
|
|
|
|
if (this.handlers[handlerName] == null) {
|
|
SystemLogger.error(`Upload handler "${handlerName}" does not exists`);
|
|
}
|
|
return this.handlers[handlerName];
|
|
},
|
|
|
|
get(file: IUpload, req: http.IncomingMessage, res: http.ServerResponse, next: NextFunction) {
|
|
const store = this.getStoreByName(file.store);
|
|
if (store?.get) {
|
|
return store.get(file, req, res, next);
|
|
}
|
|
res.writeHead(404);
|
|
res.end();
|
|
},
|
|
|
|
async getBuffer(file: IUpload): Promise<Buffer> {
|
|
const store = this.getStoreByName(file.store);
|
|
|
|
if (!store?.get) {
|
|
throw new Error('Store is invalid');
|
|
}
|
|
|
|
const buffer = new streamBuffers.WritableStreamBuffer({
|
|
initialSize: file.size,
|
|
});
|
|
|
|
return new Promise((resolve, reject) => {
|
|
buffer.on('finish', () => {
|
|
const contents = buffer.getContents();
|
|
if (contents === false) {
|
|
return reject();
|
|
}
|
|
resolve(contents);
|
|
});
|
|
|
|
void store.copy?.(file, buffer);
|
|
});
|
|
},
|
|
|
|
async copy(file: IUpload, targetFile: string) {
|
|
const store = this.getStoreByName(file.store);
|
|
const out = fs.createWriteStream(targetFile);
|
|
|
|
file = FileUpload.addExtensionTo(file);
|
|
|
|
if (store.copy) {
|
|
await store.copy(file, out);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
redirectToFile(fileUrl: string, _req: http.IncomingMessage, res: http.ServerResponse) {
|
|
res.removeHeader('Content-Length');
|
|
res.removeHeader('Cache-Control');
|
|
res.setHeader('Location', fileUrl);
|
|
res.writeHead(302);
|
|
res.end();
|
|
},
|
|
|
|
proxyFile(
|
|
fileName: string,
|
|
fileUrl: string,
|
|
forceDownload: boolean,
|
|
request: typeof http | typeof https,
|
|
_req: http.IncomingMessage,
|
|
res: http.ServerResponse,
|
|
) {
|
|
res.setHeader('Content-Disposition', `${forceDownload ? 'attachment' : 'inline'}; filename="${encodeURI(fileName)}"`);
|
|
|
|
request.get(fileUrl, (fileRes) => fileRes.pipe(res));
|
|
},
|
|
|
|
generateJWTToFileUrls({ rid, userId, fileId }: { rid: string; userId: string; fileId: string }) {
|
|
if (!settings.get('FileUpload_ProtectFiles') || !settings.get('FileUpload_Enable_json_web_token_for_files')) {
|
|
return;
|
|
}
|
|
return generateJWT(
|
|
{
|
|
rid,
|
|
userId,
|
|
fileId,
|
|
},
|
|
settings.get('FileUpload_json_web_token_secret_for_files'),
|
|
);
|
|
},
|
|
|
|
async removeFilesByRoomId(rid: string) {
|
|
if (typeof rid !== 'string' || rid.trim().length === 0) {
|
|
return;
|
|
}
|
|
const cursor = Messages.find(
|
|
{
|
|
rid,
|
|
'file._id': {
|
|
$exists: true,
|
|
},
|
|
},
|
|
{
|
|
projection: {
|
|
'file._id': 1,
|
|
},
|
|
},
|
|
);
|
|
|
|
for await (const document of cursor) {
|
|
if (document.file) {
|
|
await FileUpload.getStore('Uploads').deleteById(document.file._id);
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
type FileUploadClassOptions = {
|
|
name: string;
|
|
model?: typeof Avatars | typeof Uploads | typeof UserDataFiles;
|
|
store?: Store;
|
|
get: (file: IUpload, req: http.IncomingMessage, res: http.ServerResponse, next: NextFunction) => Promise<void>;
|
|
insert?: () => Promise<IUpload>;
|
|
getStore?: () => Store;
|
|
copy?: (file: IUpload, out: WriteStream | WritableStreamBuffer) => Promise<void>;
|
|
};
|
|
|
|
export class FileUploadClass {
|
|
public name: FileUploadClassOptions['name'];
|
|
|
|
public model: typeof Avatars | typeof Uploads | typeof UserDataFiles;
|
|
|
|
public _store: Store;
|
|
|
|
public get: FileUploadClassOptions['get'];
|
|
|
|
public copy: FileUploadClassOptions['copy'];
|
|
|
|
constructor({ name, model, store, get, insert, getStore, copy }: FileUploadClassOptions) {
|
|
this.name = name;
|
|
this.model = model || this.getModelFromName();
|
|
this._store = store || UploadFS.getStore(name);
|
|
this.get = get;
|
|
this.copy = copy;
|
|
|
|
if (insert) {
|
|
this.insert = insert;
|
|
}
|
|
|
|
if (getStore) {
|
|
this.getStore = getStore;
|
|
}
|
|
|
|
FileUpload.handlers[name] = this;
|
|
}
|
|
|
|
getStore() {
|
|
return this._store;
|
|
}
|
|
|
|
get store() {
|
|
return this.getStore();
|
|
}
|
|
|
|
set store(store) {
|
|
this._store = store;
|
|
}
|
|
|
|
getModelFromName(): typeof this.model {
|
|
const modelsAvailable: Record<string, typeof this.model> = {
|
|
Avatars,
|
|
Uploads,
|
|
UserDataFiles,
|
|
};
|
|
const modelName = this.name.split(':')[1];
|
|
if (!modelsAvailable[modelName]) {
|
|
throw new Error('Invalid Model for FileUpload');
|
|
}
|
|
return modelsAvailable[modelName];
|
|
}
|
|
|
|
async delete(fileId: string) {
|
|
// TODO: Remove this method
|
|
if (this.store?.delete) {
|
|
await this.store.delete(fileId);
|
|
}
|
|
|
|
return this.model.deleteFile(fileId);
|
|
}
|
|
|
|
async deleteById(fileId: string) {
|
|
const file = await this.model.findOneById(fileId);
|
|
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
const store = FileUpload.getStoreByName(file.store);
|
|
|
|
return store.delete(file._id);
|
|
}
|
|
|
|
async deleteByName(fileName: string) {
|
|
const file = await this.model.findOneByName(fileName);
|
|
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
const store = FileUpload.getStoreByName(file.store);
|
|
|
|
return store.delete(file._id);
|
|
}
|
|
|
|
async deleteByRoomId(rid: string) {
|
|
const file = await this.model.findOneByRoomId(rid);
|
|
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
const store = FileUpload.getStoreByName(file.store);
|
|
|
|
return store.delete(file._id);
|
|
}
|
|
|
|
async _doInsert(fileData: OptionalId<IUpload>, streamOrBuffer: ReadableStream | stream | Buffer): Promise<IUpload> {
|
|
const fileId = await this.store.create(fileData);
|
|
const tmpFile = UploadFS.getTempFilePath(fileId);
|
|
|
|
try {
|
|
if (streamOrBuffer instanceof stream) {
|
|
streamOrBuffer.pipe(fs.createWriteStream(tmpFile));
|
|
} else if (streamOrBuffer instanceof Buffer) {
|
|
fs.writeFileSync(tmpFile, streamOrBuffer);
|
|
} else {
|
|
throw new Error('Invalid file type');
|
|
}
|
|
|
|
const file = await ufsComplete(fileId, this.name);
|
|
|
|
return file;
|
|
} catch (e: any) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async insert(fileData: OptionalId<IUpload>, streamOrBuffer: ReadableStream | stream.Readable | Buffer) {
|
|
if (streamOrBuffer instanceof stream) {
|
|
streamOrBuffer = await streamToBuffer(streamOrBuffer);
|
|
}
|
|
|
|
if (streamOrBuffer instanceof Uint8Array) {
|
|
// Services compat :)
|
|
streamOrBuffer = Buffer.from(streamOrBuffer);
|
|
}
|
|
|
|
// Check if the fileData matches store filter
|
|
const filter = this.store.getFilter();
|
|
if (filter?.check) {
|
|
await filter.check(fileData, streamOrBuffer);
|
|
}
|
|
|
|
return this._doInsert(fileData, streamOrBuffer);
|
|
}
|
|
}
|
|
|