text: 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 { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions';
  
  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';
  import { AppEvents, Apps } from '../../../apps/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, stream }) {
  		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');
  		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);
  		}
  
  		// App IPreFileUpload event hook
  		try {
  			Promise.await(Apps.triggerEvent(AppEvents.IPreFileUpload, { file, stream }));
  		} catch (error) {
  			if (error instanceof AppsEngineException) {
  				throw new Meteor.Error('error-app-prevented', error.message);
  			}
  
  			throw error;
  		}
  
  		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) {
  				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) {
  				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 }) {
  		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);
  
  		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 = {} }) {
  		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']);
  		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) {
  		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;
  
  		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({ file: fileData, stream: streamOrBuffer });
  		}
  
  		return this._doInsert(fileData, streamOrBuffer, cb);
  	}
  }
  