The communications platform that puts data protection first.
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.
Rocket.Chat/app/file-upload/server/lib/FileUpload.js

597 lines
16 KiB

import fs from 'fs';
import stream from 'stream';
import { Meteor } from 'meteor/meteor';
import streamBuffers from 'stream-buffers';
import Future from 'fibers/future';
import sharp from 'sharp';
import { Cookies } from 'meteor/ostrio:cookies';
import { UploadFS } from 'meteor/jalik:ufs';
import { Match } from 'meteor/check';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import filesize from 'filesize';
import { settings } from '../../../settings/server';
import Uploads from '../../../models/server/models/Uploads';
import UserDataFiles from '../../../models/server/models/UserDataFiles';
import Avatars from '../../../models/server/models/Avatars';
import Users from '../../../models/server/models/Users';
import Rooms from '../../../models/server/models/Rooms';
import Settings from '../../../models/server/models/Settings';
import { mime } from '../../../utils/lib/mimeTypes';
import { roomTypes } from '../../../utils/server/lib/roomTypes';
import { hasPermission } from '../../../authorization/server/functions/hasPermission';
import { canAccessRoom } from '../../../authorization/server/functions/canAccessRoom';
import { fileUploadIsValidContentType } from '../../../utils/lib/fileUploadRestrictions';
import { isValidJWT, generateJWT } from '../../../utils/server/lib/JWTHelper';
import { Messages } from '../../../models/server';
const cookie = new Cookies();
let maxFileSize = 0;
settings.get('FileUpload_MaxFileSize', function(key, value) {
try {
maxFileSize = parseInt(value);
} catch (e) {
maxFileSize = Settings.findOneById('FileUpload_MaxFileSize').packageValue;
}
});
export const FileUpload = {
handlers: {},
getPath(path = '') {
return `/file-upload/${ path }`;
},
configureUploadsStore(store, name, options) {
const type = name.split(':').pop();
const stores = UploadFS.getStores();
delete stores[name];
return new UploadFS.store[store](Object.assign({
name,
}, options, FileUpload[`default${ type }`]()));
},
validateFileUpload(file) {
if (!Match.test(file.rid, String)) {
return false;
}
// livechat users can upload files but they don't have an userId
const user = file.userId ? Meteor.users.findOne(file.userId) : null;
const room = Rooms.findOneById(file.rid);
const directMessageAllowed = settings.get('FileUpload_Enabled_Direct');
const fileUploadAllowed = settings.get('FileUpload_Enabled');
Regression: File upload via apps not working in some scenarios (#18995) * [FIX] [Apps] Fix app user doesn't has permission to upload files * Fix errorClass [Error]: Forbidden [forbidden] In `app/file-upload/lib/FileUploadBase.js`, we configured `UploadFS.config.defaultStorePermissions`, which validates `insert(userId, doc)` etc. But the parameter userId cann't be always obtained by this validation method correctly (sometimes it's undefined). Meteor use the Meteor.userId() as its fallback option. So we can wrap the original call with `Meteor.runAsUser` to solve issue. * Add a new validator into canAccessRoom canAccessRoom (`app/authorization/server/functions/canAccessRoom.js`) is an essential validator for Rocket.Chat to check whether some user has permissions to access some room. In this PR, we added a new validator that allows app users to access any room on a Rocket.Chat server even if it is not a member of the room. * An attempt to fix Meteor code must always run within a Fiber Error Original Error: "Error: Meteor code must always run within a Fiber. Try wrapping callbacks that you pass to non-Meteor libraries with Meteor.bindEnvironment." * Add support for uploading files by a livecaht visitor * Support upload files with livechat visitors * Reduce an unnecessary DB query - Users.findOneById * Move the "bypass" out of canAccessRoom Adding a bypass inside canAccessRoom can potentially allow apps to do stuff we're not prepared (yet) * Update Apps-Engine version * Some refactoring * Fix a rateada Co-authored-by: Douglas Gubert <douglas.gubert@gmail.com>
5 years ago
if (user?.type !== 'app' && canAccessRoom(room, user, file) !== true) {
return false;
}
const language = user ? user.language : 'en';
if (!fileUploadAllowed) {
const reason = TAPi18n.__('FileUpload_Disabled', language);
throw new Meteor.Error('error-file-upload-disabled', reason);
}
if (!directMessageAllowed && room.t === 'd') {
const reason = TAPi18n.__('File_not_allowed_direct_messages', 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 > maxFileSize) {
const reason = TAPi18n.__('File_exceeds_allowed_size_of_bytes', {
size: filesize(maxFileSize),
}, language);
throw new Meteor.Error('error-file-too-large', reason);
}
if (!fileUploadIsValidContentType(file.type)) {
const reason = TAPi18n.__('File_type_is_not_accepted', language);
throw new Meteor.Error('error-invalid-file-type', reason);
}
return true;
},
validateAvatarUpload(file) {
if (!Match.test(file.rid, String) && !Match.test(file.userId, String)) {
return false;
}
const user = file.uid ? Meteor.users.findOne(file.uid, { fields: { language: 1 } }) : null;
const language = user?.language || 'en';
// accept only images
if (!/^image\//.test(file.type)) {
const reason = TAPi18n.__('File_type_is_not_accepted', language);
throw new Meteor.Error('error-invalid-file-type', reason);
}
// -1 maxFileSize means there is no limit
if (maxFileSize > -1 && file.size > maxFileSize) {
const reason = TAPi18n.__('File_exceeds_allowed_size_of_bytes', {
size: filesize(maxFileSize),
}, language);
throw new Meteor.Error('error-file-too-large', reason);
}
return true;
},
defaultUploads() {
return {
collection: Uploads.model,
filter: new UploadFS.Filter({
onCheck: FileUpload.validateFileUpload,
}),
getPath(file) {
Convert rocketchat-file-upload to main module structure (#13094) * Move rocketchat settings to specific package * WIP: Move models from rocketchat-lib to a specific package (server) * Move function from rocketchat:lib to rocketchat:utils to use it in rocketchat:models * Move client models from rocketchat:lib to rocketchat:models * Fix lint * Move rocketchat.info from lib to utils * Remove directly dependency between lib and migrations * Move statistics Model to rocketchat:models * Create rocketchat:metrics to be able to depacking rocketchat callbacks * Move callbacks to specific package * Remove unused dependency * Move rocketchat-notifications to a specific package * Move rocketchat-promises to a specific package * remove directly dependency from metrics and models * Move CachedCollection from lib to models * Move ui models/collections from ui to models * Move authorization client/ui models to rocketchat:models to be able to remove lib dependency * Creation of rocketchat:ui-utils to help decouple rocketchat:lib and rocketchat:authz * Move some common functions to rocketchat:utils * Change imports to dynamic imports to avoid directly dependency between some packages * Move authz models to rocketchat:models * Remove directly dependency between rocketchat:authz and rocketchat:lib * Move some functions from rocketchat:lib to rocketchat:utils * Add functions to settings package * Convert rocketchat:file-upload to main module structure * Import FileUpload where it is being used * Remove FileUpload and fileUploadHandler from globals eslintrc * Merge branch 'develop' into globals/move-rocketchat-callbacks * Fix missed export * Fix canBeDeleted and canBeCreated function, remove async
7 years ago
return `${ settings.get('uniqueID') }/uploads/${ file.rid }/${ file.userId }/${ file._id }`;
},
onValidate: FileUpload.uploadsOnValidate,
onRead(fileId, file, req, res) {
if (!FileUpload.requestCanAccessFiles(req)) {
res.writeHead(403);
return false;
}
res.setHeader('content-disposition', `attachment; filename="${ encodeURIComponent(file.name) }"`);
return true;
},
};
},
defaultAvatars() {
return {
collection: Avatars.model,
filter: new UploadFS.Filter({
onCheck: FileUpload.validateAvatarUpload,
}),
getPath(file) {
const avatarFile = file.rid ? `room-${ file.rid }` : file.userId;
return `${ settings.get('uniqueID') }/avatars/${ avatarFile }`;
},
onValidate: FileUpload.avatarsOnValidate,
onFinishUpload: FileUpload.avatarsOnFinishUpload,
};
},
defaultUserDataFiles() {
return {
collection: UserDataFiles.model,
getPath(file) {
Convert rocketchat-file-upload to main module structure (#13094) * Move rocketchat settings to specific package * WIP: Move models from rocketchat-lib to a specific package (server) * Move function from rocketchat:lib to rocketchat:utils to use it in rocketchat:models * Move client models from rocketchat:lib to rocketchat:models * Fix lint * Move rocketchat.info from lib to utils * Remove directly dependency between lib and migrations * Move statistics Model to rocketchat:models * Create rocketchat:metrics to be able to depacking rocketchat callbacks * Move callbacks to specific package * Remove unused dependency * Move rocketchat-notifications to a specific package * Move rocketchat-promises to a specific package * remove directly dependency from metrics and models * Move CachedCollection from lib to models * Move ui models/collections from ui to models * Move authorization client/ui models to rocketchat:models to be able to remove lib dependency * Creation of rocketchat:ui-utils to help decouple rocketchat:lib and rocketchat:authz * Move some common functions to rocketchat:utils * Change imports to dynamic imports to avoid directly dependency between some packages * Move authz models to rocketchat:models * Remove directly dependency between rocketchat:authz and rocketchat:lib * Move some functions from rocketchat:lib to rocketchat:utils * Add functions to settings package * Convert rocketchat:file-upload to main module structure * Import FileUpload where it is being used * Remove FileUpload and fileUploadHandler from globals eslintrc * Merge branch 'develop' into globals/move-rocketchat-callbacks * Fix missed export * Fix canBeDeleted and canBeCreated function, remove async
7 years ago
return `${ settings.get('uniqueID') }/uploads/userData/${ file.userId }`;
},
onValidate: FileUpload.uploadsOnValidate,
onRead(fileId, file, req, res) {
if (!FileUpload.requestCanAccessFiles(req)) {
res.writeHead(403);
return false;
}
res.setHeader('content-disposition', `attachment; filename="${ encodeURIComponent(file.name) }"`);
return true;
},
};
},
avatarsOnValidate(file) {
Convert rocketchat-file-upload to main module structure (#13094) * Move rocketchat settings to specific package * WIP: Move models from rocketchat-lib to a specific package (server) * Move function from rocketchat:lib to rocketchat:utils to use it in rocketchat:models * Move client models from rocketchat:lib to rocketchat:models * Fix lint * Move rocketchat.info from lib to utils * Remove directly dependency between lib and migrations * Move statistics Model to rocketchat:models * Create rocketchat:metrics to be able to depacking rocketchat callbacks * Move callbacks to specific package * Remove unused dependency * Move rocketchat-notifications to a specific package * Move rocketchat-promises to a specific package * remove directly dependency from metrics and models * Move CachedCollection from lib to models * Move ui models/collections from ui to models * Move authorization client/ui models to rocketchat:models to be able to remove lib dependency * Creation of rocketchat:ui-utils to help decouple rocketchat:lib and rocketchat:authz * Move some common functions to rocketchat:utils * Change imports to dynamic imports to avoid directly dependency between some packages * Move authz models to rocketchat:models * Remove directly dependency between rocketchat:authz and rocketchat:lib * Move some functions from rocketchat:lib to rocketchat:utils * Add functions to settings package * Convert rocketchat:file-upload to main module structure * Import FileUpload where it is being used * Remove FileUpload and fileUploadHandler from globals eslintrc * Merge branch 'develop' into globals/move-rocketchat-callbacks * Fix missed export * Fix canBeDeleted and canBeCreated function, remove async
7 years ago
if (settings.get('Accounts_AvatarResize') !== true) {
return;
}
if (file.rid) {
if (!hasPermission(Meteor.userId(), 'edit-room-avatar', file.rid)) {
throw new Meteor.Error('error-not-allowed', 'Change avatar is not allowed');
}
} else if (Meteor.userId() !== file.userId && !hasPermission(Meteor.userId(), 'edit-other-user-info')) {
throw new Meteor.Error('error-not-allowed', 'Change avatar is not allowed');
}
const tempFilePath = UploadFS.getTempFilePath(file._id);
Convert rocketchat-file-upload to main module structure (#13094) * Move rocketchat settings to specific package * WIP: Move models from rocketchat-lib to a specific package (server) * Move function from rocketchat:lib to rocketchat:utils to use it in rocketchat:models * Move client models from rocketchat:lib to rocketchat:models * Fix lint * Move rocketchat.info from lib to utils * Remove directly dependency between lib and migrations * Move statistics Model to rocketchat:models * Create rocketchat:metrics to be able to depacking rocketchat callbacks * Move callbacks to specific package * Remove unused dependency * Move rocketchat-notifications to a specific package * Move rocketchat-promises to a specific package * remove directly dependency from metrics and models * Move CachedCollection from lib to models * Move ui models/collections from ui to models * Move authorization client/ui models to rocketchat:models to be able to remove lib dependency * Creation of rocketchat:ui-utils to help decouple rocketchat:lib and rocketchat:authz * Move some common functions to rocketchat:utils * Change imports to dynamic imports to avoid directly dependency between some packages * Move authz models to rocketchat:models * Remove directly dependency between rocketchat:authz and rocketchat:lib * Move some functions from rocketchat:lib to rocketchat:utils * Add functions to settings package * Convert rocketchat:file-upload to main module structure * Import FileUpload where it is being used * Remove FileUpload and fileUploadHandler from globals eslintrc * Merge branch 'develop' into globals/move-rocketchat-callbacks * Fix missed export * Fix canBeDeleted and canBeCreated function, remove async
7 years ago
const height = settings.get('Accounts_AvatarSize');
const width = height;
const future = new Future();
const s = sharp(tempFilePath);
s.rotate();
s.metadata(Meteor.bindEnvironment((err, metadata) => {
if (!metadata) {
metadata = {};
}
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 })
.then(Meteor.bindEnvironment(({ data, info }) => {
fs.writeFile(tempFilePath, data, Meteor.bindEnvironment((err) => {
if (err != null) {
console.error(err);
}
this.getCollection().direct.update({ _id: file._id }, {
$set: {
size: info.size,
...['gif', 'svg'].includes(metadata.format) ? { type: 'image/png' } : {},
},
});
future.return();
}));
}));
}));
return future.wait();
},
resizeImagePreview(file) {
file = Uploads.findOneById(file._id);
file = FileUpload.addExtensionTo(file);
const image = 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;
},
uploadsOnValidate(file) {
if (!/^image\/((x-windows-)?bmp|p?jpeg|png)$/.test(file.type)) {
return;
}
const tmpFile = UploadFS.getTempFilePath(file._id);
const fut = new Future();
const s = sharp(tmpFile);
s.metadata(Meteor.bindEnvironment((err, metadata) => {
if (err != null) {
console.error(err);
return fut.return();
}
const identify = {
format: metadata.format,
size: {
width: metadata.width,
height: metadata.height,
},
};
const reorientation = (cb) => {
if (!metadata.orientation || metadata.orientation === 1 || settings.get('FileUpload_RotateImages') !== true) {
return cb();
}
s.rotate()
.toFile(`${ tmpFile }.tmp`)
.then(Meteor.bindEnvironment(() => {
fs.unlink(tmpFile, Meteor.bindEnvironment(() => {
fs.rename(`${ tmpFile }.tmp`, tmpFile, Meteor.bindEnvironment(() => {
cb();
}));
}));
})).catch((err) => {
console.error(err);
fut.return();
});
};
reorientation(() => {
const { size } = fs.lstatSync(tmpFile);
this.getCollection().direct.update({ _id: file._id }, {
$set: { size, identify },
});
fut.return();
});
}));
return fut.wait();
},
avatarRoomOnFinishUpload(file) {
if (!hasPermission(Meteor.userId(), 'edit-room-avatar', file.rid)) {
throw new Meteor.Error('error-not-allowed', 'Change avatar is not allowed');
}
},
avatarsOnFinishUpload(file) {
if (file.rid) {
return FileUpload.avatarRoomOnFinishUpload(file);
}
if (Meteor.userId() !== file.userId && !hasPermission(Meteor.userId(), 'edit-other-user-info')) {
throw new Meteor.Error('error-not-allowed', 'Change avatar is not allowed');
}
// update file record to match user's username
const user = Users.findOneById(file.userId);
const oldAvatar = Avatars.findOneByName(user.username);
if (oldAvatar) {
Avatars.deleteFile(oldAvatar._id);
}
Avatars.updateFileNameById(file._id, user.username);
// console.log('upload finished ->', file);
},
requestCanAccessFiles({ headers = {}, query = {} }) {
Convert rocketchat-file-upload to main module structure (#13094) * Move rocketchat settings to specific package * WIP: Move models from rocketchat-lib to a specific package (server) * Move function from rocketchat:lib to rocketchat:utils to use it in rocketchat:models * Move client models from rocketchat:lib to rocketchat:models * Fix lint * Move rocketchat.info from lib to utils * Remove directly dependency between lib and migrations * Move statistics Model to rocketchat:models * Create rocketchat:metrics to be able to depacking rocketchat callbacks * Move callbacks to specific package * Remove unused dependency * Move rocketchat-notifications to a specific package * Move rocketchat-promises to a specific package * remove directly dependency from metrics and models * Move CachedCollection from lib to models * Move ui models/collections from ui to models * Move authorization client/ui models to rocketchat:models to be able to remove lib dependency * Creation of rocketchat:ui-utils to help decouple rocketchat:lib and rocketchat:authz * Move some common functions to rocketchat:utils * Change imports to dynamic imports to avoid directly dependency between some packages * Move authz models to rocketchat:models * Remove directly dependency between rocketchat:authz and rocketchat:lib * Move some functions from rocketchat:lib to rocketchat:utils * Add functions to settings package * Convert rocketchat:file-upload to main module structure * Import FileUpload where it is being used * Remove FileUpload and fileUploadHandler from globals eslintrc * Merge branch 'develop' into globals/move-rocketchat-callbacks * Fix missed export * Fix canBeDeleted and canBeCreated function, remove async
7 years ago
if (!settings.get('FileUpload_ProtectFiles')) {
return true;
}
let { rc_uid, rc_token, rc_rid, rc_room_type } = query;
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 isAuthorizedByCookies = rc_uid && rc_token && Users.findOneByIdAndLoginToken(rc_uid, rc_token);
const isAuthorizedByHeaders = headers['x-user-id'] && headers['x-auth-token'] && Users.findOneByIdAndLoginToken(headers['x-user-id'], headers['x-auth-token']);
Convert rocketchat-file-upload to main module structure (#13094) * Move rocketchat settings to specific package * WIP: Move models from rocketchat-lib to a specific package (server) * Move function from rocketchat:lib to rocketchat:utils to use it in rocketchat:models * Move client models from rocketchat:lib to rocketchat:models * Fix lint * Move rocketchat.info from lib to utils * Remove directly dependency between lib and migrations * Move statistics Model to rocketchat:models * Create rocketchat:metrics to be able to depacking rocketchat callbacks * Move callbacks to specific package * Remove unused dependency * Move rocketchat-notifications to a specific package * Move rocketchat-promises to a specific package * remove directly dependency from metrics and models * Move CachedCollection from lib to models * Move ui models/collections from ui to models * Move authorization client/ui models to rocketchat:models to be able to remove lib dependency * Creation of rocketchat:ui-utils to help decouple rocketchat:lib and rocketchat:authz * Move some common functions to rocketchat:utils * Change imports to dynamic imports to avoid directly dependency between some packages * Move authz models to rocketchat:models * Remove directly dependency between rocketchat:authz and rocketchat:lib * Move some functions from rocketchat:lib to rocketchat:utils * Add functions to settings package * Convert rocketchat:file-upload to main module structure * Import FileUpload where it is being used * Remove FileUpload and fileUploadHandler from globals eslintrc * Merge branch 'develop' into globals/move-rocketchat-callbacks * Fix missed export * Fix canBeDeleted and canBeCreated function, remove async
7 years ago
const isAuthorizedByRoom = rc_room_type && roomTypes.getConfig(rc_room_type).canAccessUploadedFile({ rc_uid, rc_rid, rc_token });
const isAuthorizedByJWT = settings.get('FileUpload_Enable_json_web_token_for_files') && token && isValidJWT(token, settings.get('FileUpload_json_web_token_secret_for_files'));
return isAuthorizedByCookies || isAuthorizedByHeaders || isAuthorizedByRoom || isAuthorizedByJWT;
},
addExtensionTo(file) {
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) {
Convert rocketchat-file-upload to main module structure (#13094) * Move rocketchat settings to specific package * WIP: Move models from rocketchat-lib to a specific package (server) * Move function from rocketchat:lib to rocketchat:utils to use it in rocketchat:models * Move client models from rocketchat:lib to rocketchat:models * Fix lint * Move rocketchat.info from lib to utils * Remove directly dependency between lib and migrations * Move statistics Model to rocketchat:models * Create rocketchat:metrics to be able to depacking rocketchat callbacks * Move callbacks to specific package * Remove unused dependency * Move rocketchat-notifications to a specific package * Move rocketchat-promises to a specific package * remove directly dependency from metrics and models * Move CachedCollection from lib to models * Move ui models/collections from ui to models * Move authorization client/ui models to rocketchat:models to be able to remove lib dependency * Creation of rocketchat:ui-utils to help decouple rocketchat:lib and rocketchat:authz * Move some common functions to rocketchat:utils * Change imports to dynamic imports to avoid directly dependency between some packages * Move authz models to rocketchat:models * Remove directly dependency between rocketchat:authz and rocketchat:lib * Move some functions from rocketchat:lib to rocketchat:utils * Add functions to settings package * Convert rocketchat:file-upload to main module structure * Import FileUpload where it is being used * Remove FileUpload and fileUploadHandler from globals eslintrc * Merge branch 'develop' into globals/move-rocketchat-callbacks * Fix missed export * Fix canBeDeleted and canBeCreated function, remove async
7 years ago
const storageType = settings.get('FileUpload_Storage_Type');
const handlerName = `${ storageType }:${ modelName }`;
return this.getStoreByName(handlerName);
},
getStoreByName(handlerName) {
if (this.handlers[handlerName] == null) {
console.error(`Upload handler "${ handlerName }" does not exists`);
}
return this.handlers[handlerName];
},
get(file, req, res, next) {
const store = this.getStoreByName(file.store);
if (store && store.get) {
return store.get(file, req, res, next);
}
res.writeHead(404);
res.end();
},
getBuffer(file, cb) {
const store = this.getStoreByName(file.store);
if (!store || !store.get) { cb(new Error('Store is invalid'), null); }
const buffer = new streamBuffers.WritableStreamBuffer({
initialSize: file.size,
});
buffer.on('finish', () => {
cb(null, buffer.getContents());
});
store.copy(file, buffer);
},
copy(file, targetFile) {
const store = this.getStoreByName(file.store);
const out = fs.createWriteStream(targetFile);
file = FileUpload.addExtensionTo(file);
if (store.copy) {
store.copy(file, out);
return true;
}
return false;
},
redirectToFile(fileUrl, req, res) {
res.removeHeader('Content-Length');
res.removeHeader('Cache-Control');
res.setHeader('Location', fileUrl);
res.writeHead(302);
res.end();
},
proxyFile(fileName, fileUrl, forceDownload, request, req, res) {
res.setHeader('Content-Disposition', `${ forceDownload ? 'attachment' : 'inline' }; filename="${ encodeURI(fileName) }"`);
request.get(fileUrl, (fileRes) => fileRes.pipe(res));
},
generateJWTToFileUrls({ rid, userId, fileId }) {
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'));
},
removeFilesByRoomId(rid) {
if (typeof rid !== 'string' || rid.trim().length === 0) {
return;
}
Messages.find({
rid,
'file._id': {
$exists: true,
},
}, {
fields: {
'file._id': 1,
},
}).fetch().forEach((document) => FileUpload.getStore('Uploads').deleteById(document.file._id));
},
};
export class FileUploadClass {
constructor({ name, model, store, get, insert, getStore, copy }) {
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;
6 years ago
this.insertSync = Meteor.wrapAsync(this.insert, this);
}
getStore() {
return this._store;
}
get store() {
return this.getStore();
}
set store(store) {
this._store = store;
}
getModelFromName() {
const modelsAvailable = {
Avatars,
Uploads,
UserDataFiles,
};
const modelName = this.name.split(':')[1];
if (!modelsAvailable[modelName]) {
throw new Error('Invalid Model for FileUpload');
}
return modelsAvailable[modelName];
}
delete(fileId) {
if (this.store && this.store.delete) {
this.store.delete(fileId);
}
return this.model.deleteFile(fileId);
}
deleteById(fileId) {
const file = this.model.findOneById(fileId);
if (!file) {
return;
}
const store = FileUpload.getStoreByName(file.store);
return store.delete(file._id);
}
deleteByName(fileName) {
const file = this.model.findOneByName(fileName);
if (!file) {
return;
}
const store = FileUpload.getStoreByName(file.store);
return store.delete(file._id);
}
deleteByRoomId(rid) {
const file = this.model.findOneByRoomId(rid);
if (!file) {
return;
}
const store = FileUpload.getStoreByName(file.store);
return store.delete(file._id);
}
_doInsert(fileData, streamOrBuffer, cb) {
const fileId = this.store.create(fileData);
const token = this.store.createToken(fileId);
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 = Meteor.call('ufsComplete', fileId, this.name, token);
if (cb) {
cb(null, file);
}
return file;
} catch (e) {
if (cb) {
cb(e);
} else {
throw e;
}
}
}
insert(fileData, streamOrBuffer, cb) {
fileData.size = parseInt(fileData.size) || 0;
// Check if the fileData matches store filter
const filter = this.store.getFilter();
if (filter && filter.check) {
filter.check(fileData);
}
return this._doInsert(fileData, streamOrBuffer, cb);
}
}