diff --git a/.eslintrc b/.eslintrc index bed88a903a88..6e652c53b845 100644 --- a/.eslintrc +++ b/.eslintrc @@ -109,6 +109,7 @@ "EJSON" : false, "Email" : false, "FlowRouter" : false, + "FileUpload" : false, "HTTP" : false, "getNextAgent" : false, "handleError" : false, @@ -132,7 +133,6 @@ "ReactiveVar" : false, "RocketChat" : true, "RocketChatFile" : false, - "RocketChatFileAvatarInstance": false, "RoomHistoryManager" : false, "RoomManager" : false, "s" : false, diff --git a/lib/fileUpload.js b/lib/fileUpload.js deleted file mode 100644 index fbb006f15b7e..000000000000 --- a/lib/fileUpload.js +++ /dev/null @@ -1,89 +0,0 @@ -/* globals UploadFS, FileUpload */ -import { Cookies } from 'meteor/ostrio:cookies'; - -if (UploadFS) { - const initFileStore = function() { - const cookie = new Cookies(); - if (Meteor.isClient) { - document.cookie = `rc_uid=${ escape(Meteor.userId()) }; path=/`; - document.cookie = `rc_token=${ escape(Accounts._storedLoginToken()) }; path=/`; - } - - Meteor.fileStore = new UploadFS.store.GridFS({ - collection: RocketChat.models.Uploads.model, - name: 'rocketchat_uploads', - collectionName: 'rocketchat_uploads', - filter: new UploadFS.Filter({ - onCheck: FileUpload.validateFileUpload - }), - transformWrite(readStream, writeStream, fileId, file) { - if (RocketChatFile.enabled === false || !/^image\/.+/.test(file.type)) { - return readStream.pipe(writeStream); - } - - let stream = undefined; - - const identify = function(err, data) { - if (err) { - return stream.pipe(writeStream); - } - - file.identify = { - format: data.format, - size: data.size - }; - - if (data.Orientation && !['', 'Unknown', 'Undefined'].includes(data.Orientation)) { - RocketChatFile.gm(stream).autoOrient().stream().pipe(writeStream); - } else { - stream.pipe(writeStream); - } - }; - - stream = RocketChatFile.gm(readStream).identify(identify).stream(); - }, - - onRead(fileId, file, req, res) { - if (RocketChat.settings.get('FileUpload_ProtectFiles')) { - let uid; - let token; - - if (req && req.headers && req.headers.cookie) { - const rawCookies = req.headers.cookie; - - if (rawCookies) { - uid = cookie.get('rc_uid', rawCookies) ; - token = cookie.get('rc_token', rawCookies); - } - } - - if (!uid) { - uid = req.query.rc_uid; - token = req.query.rc_token; - } - - if (!uid || !token || !RocketChat.models.Users.findOneByIdAndLoginToken(uid, token)) { - res.writeHead(403); - return false; - } - } - - res.setHeader('content-disposition', `attachment; filename="${ encodeURIComponent(file.name) }"`); - return true; - } - }); - }; - - Meteor.startup(function() { - if (Meteor.isServer) { - initFileStore(); - } else { - Tracker.autorun(function(c) { - if (Meteor.userId() && RocketChat.settings.cachedCollection.ready.get()) { - initFileStore(); - c.stop(); - } - }); - } - }); -} diff --git a/package.json b/package.json index 7394bdaa8b96..d468725e3764 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,8 @@ "supertest": "^3.0.0" }, "dependencies": { + "@google-cloud/storage": "^1.1.1", + "aws-sdk": "^2.55.0", "babel-runtime": "^6.23.0", "bcrypt": "^1.0.2", "codemirror": "^5.26.0", diff --git a/packages/rocketchat-api/server/api.js b/packages/rocketchat-api/server/api.js index f1e71eeaa5a2..a8ee3da879d6 100644 --- a/packages/rocketchat-api/server/api.js +++ b/packages/rocketchat-api/server/api.js @@ -104,7 +104,7 @@ class API extends Restivus { try { result = originalAction.apply(this); } catch (e) { - this.logger.debug(`${ method } ${ route } threw an error:`, e); + this.logger.debug(`${ method } ${ route } threw an error:`, e.stack); return RocketChat.API.v1.failure(e.message, e.error); } diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js b/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js deleted file mode 100644 index 277f6471a9aa..000000000000 --- a/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js +++ /dev/null @@ -1,65 +0,0 @@ -/* globals FileUpload, FileUploadBase, Slingshot */ - -FileUpload.AmazonS3 = class FileUploadAmazonS3 extends FileUploadBase { - constructor(meta, file) { - super(meta, file); - this.uploader = new Slingshot.Upload('rocketchat-uploads', { rid: meta.rid }); - } - - start() { - this.uploader.send(this.file, (error, downloadUrl) => { - if (this.computation) { - this.computation.stop(); - } - - if (error) { - let uploading = Session.get('uploading'); - if (!Array.isArray(uploading)) { - uploading = []; - } - - const item = _.findWhere(uploading, { id: this.id }); - - if (_.isObject(item)) { - item.error = error.error; - item.percentage = 0; - } else { - uploading.push({ - error: error.error, - percentage: 0 - }); - } - - Session.set('uploading', uploading); - } else { - const file = _.pick(this.meta, 'type', 'size', 'name', 'identify', 'description'); - file._id = downloadUrl.substr(downloadUrl.lastIndexOf('/') + 1); - file.url = downloadUrl; - - Meteor.call('sendFileMessage', this.meta.rid, 's3', file, () => { - Meteor.setTimeout(() => { - const uploading = Session.get('uploading'); - if (uploading !== null) { - const item = _.findWhere(uploading, { - id: this.id - }); - return Session.set('uploading', _.without(uploading, item)); - } - }, 2000); - }); - } - }); - - this.computation = Tracker.autorun(() => { - this.onProgress(this.uploader.progress()); - }); - } - - onProgress() {} - - stop() { - if (this.uploader && this.uploader.xhr) { - this.uploader.xhr.abort(); - } - } -}; diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadFileSystem.js b/packages/rocketchat-file-upload/client/lib/FileUploadFileSystem.js deleted file mode 100644 index d19c5c03141f..000000000000 --- a/packages/rocketchat-file-upload/client/lib/FileUploadFileSystem.js +++ /dev/null @@ -1,64 +0,0 @@ -/* globals FileUploadBase, UploadFS, FileUpload:true, FileSystemStore:true */ - -FileSystemStore = new UploadFS.store.Local({ - collection: RocketChat.models.Uploads.model, - name: 'fileSystem', - filter: new UploadFS.Filter({ - onCheck: FileUpload.validateFileUpload - }) -}); - -FileUpload.FileSystem = class FileUploadFileSystem extends FileUploadBase { - constructor(meta, file) { - super(meta, file); - this.handler = new UploadFS.Uploader({ - store: FileSystemStore, - data: file, - file: meta, - onError: (err) => { - const uploading = Session.get('uploading'); - if (uploading != null) { - const item = _.findWhere(uploading, { - id: this.id - }); - if (item != null) { - item.error = err.reason; - item.percentage = 0; - } - return Session.set('uploading', uploading); - } - }, - onComplete: (fileData) => { - const file = _.pick(fileData, '_id', 'type', 'size', 'name', 'identify', 'description'); - - file.url = fileData.url.replace(Meteor.absoluteUrl(), '/'); - - Meteor.call('sendFileMessage', this.meta.rid, null, file, () => { - Meteor.setTimeout(() => { - const uploading = Session.get('uploading'); - if (uploading != null) { - const item = _.findWhere(uploading, { - id: this.id - }); - return Session.set('uploading', _.without(uploading, item)); - } - }, 2000); - }); - } - }); - - this.handler.onProgress = (file, progress) => { - this.onProgress(progress); - }; - } - - start() { - return this.handler.start(); - } - - onProgress() {} - - stop() { - return this.handler.stop(); - } -}; diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js b/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js deleted file mode 100644 index d1daa62ec07f..000000000000 --- a/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js +++ /dev/null @@ -1,65 +0,0 @@ -/* globals FileUpload, FileUploadBase, Slingshot */ - -FileUpload.GoogleCloudStorage = class FileUploadGoogleCloudStorage extends FileUploadBase { - constructor(meta, file) { - super(meta, file); - this.uploader = new Slingshot.Upload('rocketchat-uploads-gs', { rid: meta.rid }); - } - - start() { - this.uploader.send(this.file, (error, downloadUrl) => { - if (this.computation) { - this.computation.stop(); - } - - if (error) { - let uploading = Session.get('uploading'); - if (!Array.isArray(uploading)) { - uploading = []; - } - - const item = _.findWhere(uploading, { id: this.id }); - - if (_.isObject(item)) { - item.error = error.error; - item.percentage = 0; - } else { - uploading.push({ - error: error.error, - percentage: 0 - }); - } - - Session.set('uploading', uploading); - } else { - const file = _.pick(this.meta, 'type', 'size', 'name', 'identify', 'description'); - file._id = downloadUrl.substr(downloadUrl.lastIndexOf('/') + 1); - file.url = downloadUrl; - - Meteor.call('sendFileMessage', this.meta.rid, 'googleCloudStorage', file, () => { - Meteor.setTimeout(() => { - const uploading = Session.get('uploading'); - if (uploading !== null) { - const item = _.findWhere(uploading, { - id: this.id - }); - return Session.set('uploading', _.without(uploading, item)); - } - }, 2000); - }); - } - }); - - this.computation = Tracker.autorun(() => { - this.onProgress(this.uploader.progress()); - }); - } - - onProgress() {} - - stop() { - if (this.uploader && this.uploader.xhr) { - this.uploader.xhr.abort(); - } - } -}; diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js b/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js deleted file mode 100644 index ac14bb7fafd0..000000000000 --- a/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js +++ /dev/null @@ -1,55 +0,0 @@ -/* globals FileUploadBase, UploadFS, FileUpload:true */ -FileUpload.GridFS = class FileUploadGridFS extends FileUploadBase { - constructor(meta, file) { - super(meta, file); - this.handler = new UploadFS.Uploader({ - store: Meteor.fileStore, - data: file, - file: meta, - onError: (err) => { - const uploading = Session.get('uploading'); - if (uploading != null) { - const item = _.findWhere(uploading, { - id: this.id - }); - if (item != null) { - item.error = err.reason; - item.percentage = 0; - } - return Session.set('uploading', uploading); - } - }, - onComplete: (fileData) => { - const file = _.pick(fileData, '_id', 'type', 'size', 'name', 'identify', 'description'); - - file.url = fileData.url.replace(Meteor.absoluteUrl(), '/'); - - Meteor.call('sendFileMessage', this.meta.rid, null, file, () => { - Meteor.setTimeout(() => { - const uploading = Session.get('uploading'); - if (uploading != null) { - const item = _.findWhere(uploading, { - id: this.id - }); - return Session.set('uploading', _.without(uploading, item)); - } - }, 2000); - }); - } - }); - - this.handler.onProgress = (file, progress) => { - this.onProgress(progress); - }; - } - - start() { - return this.handler.start(); - } - - onProgress() {} - - stop() { - return this.handler.stop(); - } -}; diff --git a/packages/rocketchat-file-upload/client/lib/fileUploadHandler.js b/packages/rocketchat-file-upload/client/lib/fileUploadHandler.js index 71f5fbdf1519..73474ab5181f 100644 --- a/packages/rocketchat-file-upload/client/lib/fileUploadHandler.js +++ b/packages/rocketchat-file-upload/client/lib/fileUploadHandler.js @@ -1,10 +1,36 @@ -/* globals FileUpload, fileUploadHandler:true */ +/* globals FileUploadBase, UploadFS, fileUploadHandler:true */ /* exported fileUploadHandler */ -fileUploadHandler = (meta, file) => { - const storageType = RocketChat.settings.get('FileUpload_Storage_Type'); +new UploadFS.Store({ + collection: RocketChat.models.Uploads.model, + name: 'Uploads', + filter: new UploadFS.Filter({ + onCheck: FileUpload.validateFileUpload + }) +}); - if (FileUpload[storageType] !== undefined) { - return new FileUpload[storageType](meta, file); +new UploadFS.Store({ + collection: RocketChat.models.Avatars.model, + name: 'Avatars', + filter: new UploadFS.Filter({ + onCheck: FileUpload.validateFileUpload + }) +}); + + +fileUploadHandler = (directive, meta, file) => { + const store = UploadFS.getStore(directive); + + if (store) { + return new FileUploadBase(store, meta, file); + } else { + console.error('Invalid file store', directive); } }; + +Tracker.autorun(function() { + if (Meteor.userId()) { + document.cookie = `rc_uid=${ escape(Meteor.userId()) }; path=/`; + document.cookie = `rc_token=${ escape(Accounts._storedLoginToken()) }; path=/`; + } +}); diff --git a/packages/rocketchat-file-upload/lib/FileUploadBase.js b/packages/rocketchat-file-upload/lib/FileUploadBase.js index 24a304cf2888..486fe8592d31 100644 --- a/packages/rocketchat-file-upload/lib/FileUploadBase.js +++ b/packages/rocketchat-file-upload/lib/FileUploadBase.js @@ -15,10 +15,11 @@ UploadFS.config.defaultStorePermissions = new UploadFS.StorePermissions({ FileUploadBase = class FileUploadBase { - constructor(meta, file) { + constructor(store, meta, file) { this.id = Random.id(); this.meta = meta; this.file = file; + this.store = store; } getProgress() { @@ -29,11 +30,32 @@ FileUploadBase = class FileUploadBase { return this.meta.name; } - start() { - + start(callback) { + this.handler = new UploadFS.Uploader({ + store: this.store, + data: this.file, + file: this.meta, + onError: (err) => { + return callback(err); + }, + onComplete: (fileData) => { + const file = _.pick(fileData, '_id', 'type', 'size', 'name', 'identify', 'description'); + + file.url = fileData.url.replace(Meteor.absoluteUrl(), '/'); + return callback(null, file, this.store.options.name); + } + }); + + this.handler.onProgress = (file, progress) => { + this.onProgress(progress); + }; + + return this.handler.start(); } - stop() { + onProgress() {} + stop() { + return this.handler.stop(); } }; diff --git a/packages/rocketchat-file-upload/package.js b/packages/rocketchat-file-upload/package.js index 8ba5db02407d..85e698199a4a 100644 --- a/packages/rocketchat-file-upload/package.js +++ b/packages/rocketchat-file-upload/package.js @@ -11,12 +11,13 @@ Package.onUse(function(api) { api.use('ecmascript'); api.use('rocketchat:file'); api.use('jalik:ufs'); + api.use('jalik:ufs-gridfs'); api.use('jalik:ufs-local@0.2.5'); api.use('edgee:slingshot'); api.use('ostrio:cookies'); - api.use('peerlibrary:aws-sdk'); api.use('rocketchat:lib'); api.use('random'); + api.use('accounts-base'); api.use('underscore'); api.use('tracker'); api.use('webapp'); @@ -27,19 +28,12 @@ Package.onUse(function(api) { api.addFiles('lib/FileUpload.js'); api.addFiles('lib/FileUploadBase.js'); - api.addFiles('client/lib/FileUploadAmazonS3.js', 'client'); - api.addFiles('client/lib/FileUploadFileSystem.js', 'client'); - api.addFiles('client/lib/FileUploadGoogleStorage.js', 'client'); - api.addFiles('client/lib/FileUploadGridFS.js', 'client'); api.addFiles('client/lib/fileUploadHandler.js', 'client'); api.addFiles('server/lib/FileUpload.js', 'server'); api.addFiles('server/lib/requests.js', 'server'); - api.addFiles('server/config/configFileUploadAmazonS3.js', 'server'); - api.addFiles('server/config/configFileUploadFileSystem.js', 'server'); - api.addFiles('server/config/configFileUploadGoogleStorage.js', 'server'); - api.addFiles('server/config/configFileUploadGridFS.js', 'server'); + api.addFiles('server/config/_configUploadStorage.js', 'server'); api.addFiles('server/methods/sendFileMessage.js', 'server'); api.addFiles('server/methods/getS3FileUrl.js', 'server'); diff --git a/packages/rocketchat-file-upload/server/config/AmazonS3.js b/packages/rocketchat-file-upload/server/config/AmazonS3.js new file mode 100644 index 000000000000..569e1460c949 --- /dev/null +++ b/packages/rocketchat-file-upload/server/config/AmazonS3.js @@ -0,0 +1,60 @@ +/* globals FileUpload */ + +import { FileUploadClass } from '../lib/FileUpload'; +import '../../ufs/AmazonS3/server.js'; + +const get = function(file, req, res) { + const fileUrl = this.store.getRedirectURL(file); + + if (fileUrl) { + res.setHeader('Location', fileUrl); + res.writeHead(302); + } + res.end(); +}; + +const AmazonS3Uploads = new FileUploadClass({ + name: 'AmazonS3:Uploads', + get + // store setted bellow +}); + +const AmazonS3Avatars = new FileUploadClass({ + name: 'AmazonS3:Avatars', + get + // store setted bellow +}); + +const configure = _.debounce(function() { + const Bucket = RocketChat.settings.get('FileUpload_S3_Bucket'); + const Acl = RocketChat.settings.get('FileUpload_S3_Acl'); + const AWSAccessKeyId = RocketChat.settings.get('FileUpload_S3_AWSAccessKeyId'); + const AWSSecretAccessKey = RocketChat.settings.get('FileUpload_S3_AWSSecretAccessKey'); + const URLExpiryTimeSpan = RocketChat.settings.get('FileUpload_S3_URLExpiryTimeSpan'); + const Region = RocketChat.settings.get('FileUpload_S3_Region'); + // const CDN = RocketChat.settings.get('FileUpload_S3_CDN'); + // const BucketURL = RocketChat.settings.get('FileUpload_S3_BucketURL'); + + if (!Bucket || !AWSAccessKeyId || !AWSSecretAccessKey) { + return; + } + + const config = { + connection: { + accessKeyId: AWSAccessKeyId, + secretAccessKey: AWSSecretAccessKey, + signatureVersion: 'v4', + params: { + Bucket, + ACL: Acl + }, + region: Region + }, + URLExpiryTimeSpan + }; + + AmazonS3Uploads.store = FileUpload.configureUploadsStore('AmazonS3', AmazonS3Uploads.name, config); + AmazonS3Avatars.store = FileUpload.configureUploadsStore('AmazonS3', AmazonS3Avatars.name, config); +}, 500); + +RocketChat.settings.get(/^FileUpload_S3_/, configure); diff --git a/packages/rocketchat-file-upload/server/config/FileSystem.js b/packages/rocketchat-file-upload/server/config/FileSystem.js new file mode 100644 index 000000000000..263baad99078 --- /dev/null +++ b/packages/rocketchat-file-upload/server/config/FileSystem.js @@ -0,0 +1,83 @@ +/* globals FileUpload, UploadFS */ + +import fs from 'fs'; +import { FileUploadClass } from '../lib/FileUpload'; + +const FileSystemUploads = new FileUploadClass({ + name: 'FileSystem:Uploads', + // store setted bellow + + get(file, req, res) { + const filePath = this.store.getFilePath(file._id, file); + + try { + const stat = Meteor.wrapAsync(fs.stat)(filePath); + + if (stat && stat.isFile()) { + file = FileUpload.addExtensionTo(file); + res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${ encodeURIComponent(file.name) }`); + res.setHeader('Last-Modified', file.uploadedAt.toUTCString()); + res.setHeader('Content-Type', file.type); + res.setHeader('Content-Length', file.size); + + this.store.getReadStream(file._id, file).pipe(res); + } + } catch (e) { + res.writeHead(404); + res.end(); + return; + } + } +}); + +const FileSystemAvatars = new FileUploadClass({ + name: 'FileSystem:Avatars', + // store setted bellow + + get(file, req, res) { + const reqModifiedHeader = req.headers['if-modified-since']; + if (reqModifiedHeader) { + if (reqModifiedHeader === (file.uploadedAt && file.uploadedAt.toUTCString())) { + res.setHeader('Last-Modified', reqModifiedHeader); + res.writeHead(304); + res.end(); + return; + } + } + + const filePath = this.store.getFilePath(file._id, file); + + try { + const stat = Meteor.wrapAsync(fs.stat)(filePath); + + if (stat && stat.isFile()) { + file = FileUpload.addExtensionTo(file); + res.setHeader('Content-Disposition', 'inline'); + res.setHeader('Last-Modified', file.uploadedAt.toUTCString()); + res.setHeader('Content-Type', file.type); + res.setHeader('Content-Length', file.size); + + this.store.getReadStream(file._id, file).pipe(res); + } + } catch (e) { + res.writeHead(404); + res.end(); + return; + } + } +}); + + +const createFileSystemStore = _.debounce(function() { + const options = { + path: RocketChat.settings.get('FileUpload_FileSystemPath') //'/tmp/uploads/photos', + }; + + FileSystemUploads.store = FileUpload.configureUploadsStore('Local', FileSystemUploads.name, options); + FileSystemAvatars.store = FileUpload.configureUploadsStore('Local', FileSystemAvatars.name, options); + + // DEPRECATED backwards compatibililty (remove) + UploadFS.getStores()['fileSystem'] = UploadFS.getStores()[FileSystemUploads.name]; +}, 500); + +RocketChat.settings.get('FileUpload_FileSystemPath', createFileSystemStore); diff --git a/packages/rocketchat-file-upload/server/config/GoogleStorage.js b/packages/rocketchat-file-upload/server/config/GoogleStorage.js new file mode 100644 index 000000000000..2010cfa68b99 --- /dev/null +++ b/packages/rocketchat-file-upload/server/config/GoogleStorage.js @@ -0,0 +1,58 @@ +/* globals FileUpload */ + +import { FileUploadClass } from '../lib/FileUpload'; +import '../../ufs/GoogleStorage/server.js'; + + +const get = function(file, req, res) { + this.store.getRedirectURL(file, (err, fileUrl) => { + if (err) { + console.error(err); + } + + if (fileUrl) { + res.setHeader('Location', fileUrl); + res.writeHead(302); + } + res.end(); + }); +}; + +const GoogleCloudStorageUploads = new FileUploadClass({ + name: 'GoogleCloudStorage:Uploads', + get + // store setted bellow +}); + +const GoogleCloudStorageAvatars = new FileUploadClass({ + name: 'GoogleCloudStorage:Avatars', + get + // store setted bellow +}); + +const configure = _.debounce(function() { + const bucket = RocketChat.settings.get('FileUpload_GoogleStorage_Bucket'); + const accessId = RocketChat.settings.get('FileUpload_GoogleStorage_AccessId'); + const secret = RocketChat.settings.get('FileUpload_GoogleStorage_Secret'); + const URLExpiryTimeSpan = RocketChat.settings.get('FileUpload_S3_URLExpiryTimeSpan'); + + if (!bucket || !accessId || !secret) { + return; + } + + const config = { + connection: { + credentials: { + client_email: accessId, + private_key: secret + } + }, + bucket, + URLExpiryTimeSpan + }; + + GoogleCloudStorageUploads.store = FileUpload.configureUploadsStore('GoogleStorage', GoogleCloudStorageUploads.name, config); + GoogleCloudStorageAvatars.store = FileUpload.configureUploadsStore('GoogleStorage', GoogleCloudStorageAvatars.name, config); +}, 500); + +RocketChat.settings.get(/^FileUpload_GoogleStorage_/, configure); diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js b/packages/rocketchat-file-upload/server/config/GridFS.js similarity index 64% rename from packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js rename to packages/rocketchat-file-upload/server/config/GridFS.js index 21ac212db8ed..ce312f11d866 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js +++ b/packages/rocketchat-file-upload/server/config/GridFS.js @@ -1,7 +1,13 @@ /* globals FileUpload, UploadFS */ -const stream = Npm.require('stream'); -const zlib = Npm.require('zlib'); -const util = Npm.require('util'); +import stream from 'stream'; +import zlib from 'zlib'; +import util from 'util'; + +import { FileUploadClass } from '../lib/FileUpload'; +import { Cookies } from 'meteor/ostrio:cookies'; + +const cookie = new Cookies(); + const logger = new Logger('FileUpload'); function ExtractRange(options) { @@ -124,7 +130,52 @@ const readFromGridFS = function(storeName, fileId, file, headers, req, res) { } }; -FileUpload.addHandler('rocketchat_uploads', { +const onRead = function(fileId, file, req, res) { + if (RocketChat.settings.get('FileUpload_ProtectFiles')) { + let uid; + let token; + + if (req && req.headers && req.headers.cookie) { + const rawCookies = req.headers.cookie; + + if (rawCookies) { + uid = cookie.get('rc_uid', rawCookies) ; + token = cookie.get('rc_token', rawCookies); + } + } + + if (!uid) { + uid = req.query.rc_uid; + token = req.query.rc_token; + } + + if (!uid || !token || !RocketChat.models.Users.findOneByIdAndLoginToken(uid, token)) { + res.writeHead(403); + return false; + } + } + + res.setHeader('content-disposition', `attachment; filename="${ encodeURIComponent(file.name) }"`); + return true; +}; + +FileUpload.configureUploadsStore('GridFS', 'GridFS:Uploads', { + collectionName: 'rocketchat_uploads', + onRead +}); + +// DEPRECATED: backwards compatibility (remove) +UploadFS.getStores()['rocketchat_uploads'] = UploadFS.getStores()['GridFS:Uploads']; + +FileUpload.configureUploadsStore('GridFS', 'GridFS:Avatars', { + collectionName: 'rocketchat_avatars', + onRead +}); + + +new FileUploadClass({ + name: 'GridFS:Uploads', + get(file, req, res) { file = FileUpload.addExtensionTo(file); const headers = { @@ -134,8 +185,32 @@ FileUpload.addHandler('rocketchat_uploads', { 'Content-Length': file.size }; return readFromGridFS(file.store, file._id, file, headers, req, res); - }, - delete(file) { - return Meteor.fileStore.delete(file._id); + } +}); + +new FileUploadClass({ + name: 'GridFS:Avatars', + + get(file, req, res) { + const reqModifiedHeader = req.headers['if-modified-since']; + if (reqModifiedHeader) { + if (reqModifiedHeader === (file.uploadedAt && file.uploadedAt.toUTCString())) { + res.setHeader('Last-Modified', reqModifiedHeader); + res.writeHead(304); + res.end(); + return; + } + } + + file = FileUpload.addExtensionTo(file); + const headers = { + 'Cache-Control': 'public, max-age=0', + 'Expires': '-1', + 'Content-Disposition': 'inline', + 'Last-Modified': file.uploadedAt.toUTCString(), + 'Content-Type': file.type, + 'Content-Length': file.size + }; + return readFromGridFS(file.store, file._id, file, headers, req, res); } }); diff --git a/packages/rocketchat-file-upload/server/config/Slingshot_DEPRECATED.js b/packages/rocketchat-file-upload/server/config/Slingshot_DEPRECATED.js new file mode 100644 index 000000000000..45d245a4d3a4 --- /dev/null +++ b/packages/rocketchat-file-upload/server/config/Slingshot_DEPRECATED.js @@ -0,0 +1,114 @@ +/* globals Slingshot, FileUpload */ + +const configureSlingshot = _.debounce(() => { + const type = RocketChat.settings.get('FileUpload_Storage_Type'); + const bucket = RocketChat.settings.get('FileUpload_S3_Bucket'); + const acl = RocketChat.settings.get('FileUpload_S3_Acl'); + const accessKey = RocketChat.settings.get('FileUpload_S3_AWSAccessKeyId'); + const secretKey = RocketChat.settings.get('FileUpload_S3_AWSSecretAccessKey'); + const cdn = RocketChat.settings.get('FileUpload_S3_CDN'); + const region = RocketChat.settings.get('FileUpload_S3_Region'); + const bucketUrl = RocketChat.settings.get('FileUpload_S3_BucketURL'); + + delete Slingshot._directives['rocketchat-uploads']; + + if (type === 'AmazonS3' && !_.isEmpty(bucket) && !_.isEmpty(accessKey) && !_.isEmpty(secretKey)) { + if (Slingshot._directives['rocketchat-uploads']) { + delete Slingshot._directives['rocketchat-uploads']; + } + const config = { + bucket, + key(file, metaContext) { + const id = Random.id(); + const path = `${ RocketChat.settings.get('uniqueID') }/uploads/${ metaContext.rid }/${ this.userId }/${ id }`; + + const upload = { + _id: id, + rid: metaContext.rid, + AmazonS3: { + path + } + }; + + RocketChat.models.Uploads.insertFileInit(this.userId, 'AmazonS3:Uploads', file, upload); + + return path; + }, + AWSAccessKeyId: accessKey, + AWSSecretAccessKey: secretKey + }; + + if (!_.isEmpty(acl)) { + config.acl = acl; + } + + if (!_.isEmpty(cdn)) { + config.cdn = cdn; + } + + if (!_.isEmpty(region)) { + config.region = region; + } + + if (!_.isEmpty(bucketUrl)) { + config.bucketUrl = bucketUrl; + } + + try { + Slingshot.createDirective('rocketchat-uploads', Slingshot.S3Storage, config); + } catch (e) { + console.error('Error configuring S3 ->', e.message); + } + } +}, 500); + +RocketChat.settings.get('FileUpload_Storage_Type', configureSlingshot); +RocketChat.settings.get(/^FileUpload_S3_/, configureSlingshot); + + + +const createGoogleStorageDirective = _.debounce(() => { + const type = RocketChat.settings.get('FileUpload_Storage_Type'); + const bucket = RocketChat.settings.get('FileUpload_GoogleStorage_Bucket'); + const accessId = RocketChat.settings.get('FileUpload_GoogleStorage_AccessId'); + const secret = RocketChat.settings.get('FileUpload_GoogleStorage_Secret'); + + delete Slingshot._directives['rocketchat-uploads-gs']; + + if (type === 'GoogleCloudStorage' && !_.isEmpty(secret) && !_.isEmpty(accessId) && !_.isEmpty(bucket)) { + if (Slingshot._directives['rocketchat-uploads-gs']) { + delete Slingshot._directives['rocketchat-uploads-gs']; + } + + const config = { + bucket, + GoogleAccessId: accessId, + GoogleSecretKey: secret, + key(file, metaContext) { + const id = Random.id(); + const path = `${ RocketChat.settings.get('uniqueID') }/uploads/${ metaContext.rid }/${ this.userId }/${ id }`; + + const upload = { + _id: id, + rid: metaContext.rid, + GoogleStorage: { + path + } + }; + + RocketChat.models.Uploads.insertFileInit(this.userId, 'GoogleCloudStorage:Uploads', file, upload); + + return path; + } + }; + + try { + Slingshot.createDirective('rocketchat-uploads-gs', Slingshot.GoogleCloud, config); + } catch (e) { + console.error('Error configuring GoogleCloudStorage ->', e.message); + } + } +}, 500); + +RocketChat.settings.get('FileUpload_Storage_Type', createGoogleStorageDirective); +RocketChat.settings.get(/^FileUpload_GoogleStorage_/, createGoogleStorageDirective); diff --git a/packages/rocketchat-file-upload/server/config/_configUploadStorage.js b/packages/rocketchat-file-upload/server/config/_configUploadStorage.js new file mode 100644 index 000000000000..df56dc4d307a --- /dev/null +++ b/packages/rocketchat-file-upload/server/config/_configUploadStorage.js @@ -0,0 +1,19 @@ +/* globals UploadFS */ + +import './AmazonS3.js'; +import './FileSystem.js'; +import './GoogleStorage.js'; +import './GridFS.js'; +import './Slingshot_DEPRECATED.js'; + +const configStore = _.debounce(() => { + const store = RocketChat.settings.get('FileUpload_Storage_Type'); + + if (store) { + console.log('Setting default file store to', store); + UploadFS.getStores().Avatars = UploadFS.getStore(`${ store }:Avatars`); + UploadFS.getStores().Uploads = UploadFS.getStore(`${ store }:Uploads`); + } +}, 1000); + +RocketChat.settings.get(/^FileUpload_/, configStore); diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js deleted file mode 100644 index e217c963e3f5..000000000000 --- a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js +++ /dev/null @@ -1,141 +0,0 @@ -/* globals Slingshot, FileUpload, AWS, SystemLogger */ -import AWS4 from '../lib/AWS4.js'; - -let S3accessKey; -let S3secretKey; -let S3expiryTimeSpan; - -const generateURL = function(file) { - if (!file || !file.s3) { - return; - } - - const credential = { - accessKeyId: S3accessKey, - secretKey: S3secretKey - }; - - const req = { - bucket: file.s3.bucket, - region: file.s3.region, - path: `/${ file.s3.path }${ file._id }`, - url: file.url, - expire: Math.max(5, S3expiryTimeSpan) - }; - - const queryString = AWS4.sign(req, credential); - - return `${ file.url }?${ queryString }`; -}; - -FileUpload.addHandler('s3', { - get(file, req, res) { - const fileUrl = generateURL(file); - - if (fileUrl) { - res.setHeader('Location', fileUrl); - res.writeHead(302); - } - res.end(); - }, - delete(file) { - const s3 = new AWS.S3(); - const request = s3.deleteObject({ - Bucket: file.s3.bucket, - Key: file.s3.path + file._id - }); - request.send(); - } -}); - -const createS3Directive = _.debounce(() => { - const directiveName = 'rocketchat-uploads'; - - const type = RocketChat.settings.get('FileUpload_Storage_Type'); - const bucket = RocketChat.settings.get('FileUpload_S3_Bucket'); - const acl = RocketChat.settings.get('FileUpload_S3_Acl'); - const accessKey = RocketChat.settings.get('FileUpload_S3_AWSAccessKeyId'); - const secretKey = RocketChat.settings.get('FileUpload_S3_AWSSecretAccessKey'); - const cdn = RocketChat.settings.get('FileUpload_S3_CDN'); - const region = RocketChat.settings.get('FileUpload_S3_Region'); - const bucketUrl = RocketChat.settings.get('FileUpload_S3_BucketURL'); - - AWS.config.update({ - accessKeyId: RocketChat.settings.get('FileUpload_S3_AWSAccessKeyId'), - secretAccessKey: RocketChat.settings.get('FileUpload_S3_AWSSecretAccessKey') - }); - - if (type === 'AmazonS3' && !_.isEmpty(bucket) && !_.isEmpty(accessKey) && !_.isEmpty(secretKey)) { - if (Slingshot._directives[directiveName]) { - delete Slingshot._directives[directiveName]; - } - const config = { - bucket, - AWSAccessKeyId: accessKey, - AWSSecretAccessKey: secretKey, - key(file, metaContext) { - const path = `${ RocketChat.hostname }/${ metaContext.rid }/${ this.userId }/`; - - const upload = { s3: { - bucket, - region, - path - }}; - const fileId = RocketChat.models.Uploads.insertFileInit(metaContext.rid, this.userId, 's3', file, upload); - - return path + fileId; - } - }; - - if (!_.isEmpty(acl)) { - config.acl = acl; - } - - if (!_.isEmpty(cdn)) { - config.cdn = cdn; - } - - if (!_.isEmpty(region)) { - config.region = region; - } - - if (!_.isEmpty(bucketUrl)) { - config.bucketUrl = bucketUrl; - } - - try { - Slingshot.createDirective(directiveName, Slingshot.S3Storage, config); - } catch (e) { - SystemLogger.error('Error configuring S3 ->', e.message); - } - } else if (Slingshot._directives[directiveName]) { - delete Slingshot._directives[directiveName]; - } -}, 500); - -RocketChat.settings.get('FileUpload_Storage_Type', createS3Directive); - -RocketChat.settings.get('FileUpload_S3_Bucket', createS3Directive); - -RocketChat.settings.get('FileUpload_S3_Acl', createS3Directive); - -RocketChat.settings.get('FileUpload_S3_AWSAccessKeyId', function(key, value) { - S3accessKey = value; - createS3Directive(); -}); - -RocketChat.settings.get('FileUpload_S3_AWSSecretAccessKey', function(key, value) { - S3secretKey = value; - createS3Directive(); -}); - -RocketChat.settings.get('FileUpload_S3_URLExpiryTimeSpan', function(key, value) { - S3expiryTimeSpan = value; - createS3Directive(); -}); - -RocketChat.settings.get('FileUpload_S3_CDN', createS3Directive); - -RocketChat.settings.get('FileUpload_S3_Region', createS3Directive); - -RocketChat.settings.get('FileUpload_S3_BucketURL', createS3Directive); diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadFileSystem.js b/packages/rocketchat-file-upload/server/config/configFileUploadFileSystem.js deleted file mode 100644 index f97534577abf..000000000000 --- a/packages/rocketchat-file-upload/server/config/configFileUploadFileSystem.js +++ /dev/null @@ -1,79 +0,0 @@ -/* globals FileSystemStore:true, FileUpload, UploadFS, RocketChatFile */ - -const storeName = 'fileSystem'; - -FileSystemStore = null; - -const createFileSystemStore = _.debounce(function() { - const stores = UploadFS.getStores(); - if (stores[storeName]) { - delete stores[storeName]; - } - FileSystemStore = new UploadFS.store.Local({ - collection: RocketChat.models.Uploads.model, - name: storeName, - path: RocketChat.settings.get('FileUpload_FileSystemPath'), //'/tmp/uploads/photos', - filter: new UploadFS.Filter({ - onCheck: FileUpload.validateFileUpload - }), - transformWrite(readStream, writeStream, fileId, file) { - if (RocketChatFile.enabled === false || !/^image\/((x-windows-)?bmp|p?jpeg|png)$/.test(file.type)) { - return readStream.pipe(writeStream); - } - - let stream = undefined; - - const identify = function(err, data) { - if (err != null) { - return stream.pipe(writeStream); - } - - file.identify = { - format: data.format, - size: data.size - }; - - if ([null, undefined, '', 'Unknown', 'Undefined'].indexOf(data.Orientation) === -1) { - return RocketChatFile.gm(stream).autoOrient().stream().pipe(writeStream); - } else { - return stream.pipe(writeStream); - } - }; - - stream = RocketChatFile.gm(readStream).identify(identify).stream(); - return; - } - }); -}, 500); - -RocketChat.settings.get('FileUpload_FileSystemPath', createFileSystemStore); - -const fs = Npm.require('fs'); - -FileUpload.addHandler(storeName, { - get(file, req, res) { - const filePath = FileSystemStore.getFilePath(file._id, file); - - try { - const stat = Meteor.wrapAsync(fs.stat)(filePath); - - if (stat && stat.isFile()) { - file = FileUpload.addExtensionTo(file); - res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${ encodeURIComponent(file.name) }`); - res.setHeader('Last-Modified', file.uploadedAt.toUTCString()); - res.setHeader('Content-Type', file.type); - res.setHeader('Content-Length', file.size); - - FileSystemStore.getReadStream(file._id, file).pipe(res); - } - } catch (e) { - res.writeHead(404); - res.end(); - return; - } - }, - - delete(file) { - return FileSystemStore.delete(file._id); - } -}); diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js b/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js deleted file mode 100644 index 9e8d5219ba60..000000000000 --- a/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js +++ /dev/null @@ -1,111 +0,0 @@ -/* globals FileUpload, Slingshot, SystemLogger */ - -const crypto = Npm.require('crypto'); - -function generateUrlParts({ file }) { - const accessId = RocketChat.settings.get('FileUpload_GoogleStorage_AccessId'); - const secret = RocketChat.settings.get('FileUpload_GoogleStorage_Secret'); - - if (!file || !file.googleCloudStorage || _.isEmpty(accessId) || _.isEmpty(secret)) { - return; - } - - return { - accessId: encodeURIComponent(accessId), - secret, - path: file.googleCloudStorage.path + file._id - }; -} - -function generateGetURL({ file }) { - const parts = generateUrlParts({ file }); - - if (!parts) { - return; - } - - const expires = new Date().getTime() + 120000; - const signature = crypto.createSign('RSA-SHA256').update(`GET\n\n\n${ expires }\n/${ file.googleCloudStorage.bucket }/${ parts.path }`).sign(parts.secret, 'base64'); - - return `${ file.url }?GoogleAccessId=${ parts.accessId }&Expires=${ expires }&Signature=${ encodeURIComponent(signature) }`; -} - -function generateDeleteUrl({ file }) { - const parts = generateUrlParts({ file }); - - if (!parts) { - return; - } - - const expires = new Date().getTime() + 5000; - const signature = crypto.createSign('RSA-SHA256').update(`DELETE\n\n\n${ expires }\n/${ file.googleCloudStorage.bucket }/${ encodeURIComponent(parts.path) }`).sign(parts.secret, 'base64'); - - return `https://${ file.googleCloudStorage.bucket }.storage.googleapis.com/${ encodeURIComponent(parts.path) }?GoogleAccessId=${ parts.accessId }&Expires=${ expires }&Signature=${ encodeURIComponent(signature) }`; -} - -FileUpload.addHandler('googleCloudStorage', { - get(file, req, res) { - const fileUrl = generateGetURL({ file }); - - if (fileUrl) { - res.setHeader('Location', fileUrl); - res.writeHead(302); - } - res.end(); - }, - delete(file) { - if (!file || !file.googleCloudStorage) { - console.warn('Failed to delete a file which is uploaded to Google Cloud Storage, the file and googleCloudStorage properties are not defined.'); - return; - } - - const url = generateDeleteUrl({ file }); - - if (_.isEmpty(url)) { - console.warn('Failed to delete a file which is uploaded to Google Cloud Storage, failed to generate a delete url.'); - return; - } - - HTTP.call('DELETE', url); - } -}); - -const createGoogleStorageDirective = _.debounce(() => { - const directiveName = 'rocketchat-uploads-gs'; - - const type = RocketChat.settings.get('FileUpload_Storage_Type'); - const bucket = RocketChat.settings.get('FileUpload_GoogleStorage_Bucket'); - const accessId = RocketChat.settings.get('FileUpload_GoogleStorage_AccessId'); - const secret = RocketChat.settings.get('FileUpload_GoogleStorage_Secret'); - - if (type === 'GoogleCloudStorage' && !_.isEmpty(secret) && !_.isEmpty(accessId) && !_.isEmpty(bucket)) { - if (Slingshot._directives[directiveName]) { - delete Slingshot._directives[directiveName]; - } - - const config = { - bucket, - GoogleAccessId: accessId, - GoogleSecretKey: secret, - key: function _googleCloudStorageKey(file, metaContext) { - const path = `${ RocketChat.settings.get('uniqueID') }/${ metaContext.rid }/${ this.userId }/`; - const fileId = RocketChat.models.Uploads.insertFileInit(metaContext.rid, this.userId, 'googleCloudStorage', file, { googleCloudStorage: { bucket, path }}); - - return path + fileId; - } - }; - - try { - Slingshot.createDirective(directiveName, Slingshot.GoogleCloud, config); - } catch (e) { - SystemLogger.error('Error configuring GoogleCloudStorage ->', e.message); - } - } else { - delete Slingshot._directives[directiveName]; - } -}, 500); - -RocketChat.settings.get('FileUpload_Storage_Type', createGoogleStorageDirective); -RocketChat.settings.get('FileUpload_GoogleStorage_Bucket', createGoogleStorageDirective); -RocketChat.settings.get('FileUpload_GoogleStorage_AccessId', createGoogleStorageDirective); -RocketChat.settings.get('FileUpload_GoogleStorage_Secret', createGoogleStorageDirective); diff --git a/packages/rocketchat-file-upload/server/lib/AWS4.js b/packages/rocketchat-file-upload/server/lib/AWS4.js deleted file mode 100644 index bb8980ac0d4c..000000000000 --- a/packages/rocketchat-file-upload/server/lib/AWS4.js +++ /dev/null @@ -1,175 +0,0 @@ -import crypto from 'crypto'; -import urllib from 'url'; -import querystring from 'querystring'; - -const Algorithm = 'AWS4-HMAC-SHA256'; -const DefaultRegion = 'us-east-1'; -const Service = 's3'; -const KeyPartsRequest = 'aws4_request'; - -class Aws4 { - constructor(req, credentials) { - const { url, method = 'GET', body = '', date, region, headers = {}, expire = 86400 } = this.req = req; - - Object.assign(this, { url, body, method: method.toUpperCase() }); - - const urlObj = urllib.parse(url); - this.region = region || DefaultRegion; - this.path = urlObj.pathname; - this.host = urlObj.host; - this.date = date || this.amzDate; - this.credentials = credentials; - this.headers = this.prepareHeaders(headers); - this.expire = expire; - } - - prepareHeaders() { - const host = this.host; - - return { - host - }; - } - - hmac(key, string, encoding) { - return crypto.createHmac('sha256', key).update(string, 'utf8').digest(encoding); - } - - hash(string, encoding = 'hex') { - return crypto.createHash('sha256').update(string, 'utf8').digest(encoding); - } - - encodeRfc3986(urlEncodedString) { - return urlEncodedString.replace(/[!'()*]/g, function(c) { - return `%${ c.charCodeAt(0).toString(16).toUpperCase() }`; - }); - } - - encodeQuery(query) { - return this.encodeRfc3986(querystring.stringify(Object.keys(query).sort().reduce((obj, key) => { - if (!key) { return obj; } - obj[key] = !Array.isArray(query[key]) ? query[key] : query[key].slice().sort(); - return obj; - }, {}))); - } - - get query() { - const query = {}; - - if (this.credentials.sessionToken) { - query['X-Amz-Security-Token'] = this.credentials.sessionToken; - } - - query['X-Amz-Expires'] = this.expire; - query['X-Amz-Date'] = this.amzDate; - query['X-Amz-Algorithm'] = Algorithm; - query['X-Amz-Credential'] = `${ this.credentials.accessKeyId }/${ this.credentialScope }`; - query['X-Amz-SignedHeaders'] = this.signedHeaders; - - return query; - } - - get amzDate() { - return (new Date()).toISOString().replace(/[:\-]|\.\d{3}/g, ''); - } - - get dateStamp() { - return this.date.slice(0, 8); - } - - get payloadHash() { - return 'UNSIGNED-PAYLOAD'; - } - - get canonicalPath() { - let pathStr = this.path; - if (pathStr === '/') { return pathStr; } - - pathStr = pathStr.replace(/\/{2,}/g, '/'); - pathStr = pathStr.split('/').reduce((path, piece) => { - if (piece === '..') { - path.pop(); - } else { - path.push(this.encodeRfc3986(querystring.escape(piece))); - } - return path; - }, []).join('/'); - - return pathStr; - } - - get canonicalQuery() { - return this.encodeQuery(this.query); - } - - get canonicalHeaders() { - const headers = Object.keys(this.headers) - .sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1) - .map(key => `${ key.toLowerCase() }:${ this.headers[key] }`); - return `${ headers.join('\n') }\n`; - } - - get signedHeaders() { - return Object.keys(this.headers) - .map(key => key.toLowerCase()) - .sort() - .join(';'); - } - - get canonicalRequest() { - return [ - this.method, - this.canonicalPath, - this.canonicalQuery, - this.canonicalHeaders, - this.signedHeaders, - this.payloadHash - ].join('\n'); - } - - get credentialScope() { - return [ - this.dateStamp, - this.region, - Service, - KeyPartsRequest - ].join('/'); - } - - get stringToSign() { - return [ - Algorithm, - this.date, - this.credentialScope, - this.hash(this.canonicalRequest) - ].join('\n'); - } - - get signingKey() { - const kDate = this.hmac(`AWS4${ this.credentials.secretKey }`, this.dateStamp); - const kRegion = this.hmac(kDate, this.region); - const kService = this.hmac(kRegion, Service); - const kSigning = this.hmac(kService, KeyPartsRequest); - - return kSigning; - } - - get signature() { - return this.hmac(this.signingKey, this.stringToSign, 'hex'); - } - - // Export - // Return signed query string - sign() { - const query = this.query; - query['X-Amz-Signature'] = this.signature; - - return this.encodeQuery(query); - } -} - -export default { - sign(request, credential) { - return (new Aws4(request, credential)).sign(); - } -}; diff --git a/packages/rocketchat-file-upload/server/lib/FileUpload.js b/packages/rocketchat-file-upload/server/lib/FileUpload.js index cb0aadbc06c9..748e8fa7336d 100644 --- a/packages/rocketchat-file-upload/server/lib/FileUpload.js +++ b/packages/rocketchat-file-upload/server/lib/FileUpload.js @@ -1,43 +1,297 @@ -/* globals FileUpload:true */ +/* globals UploadFS */ + +import fs from 'fs'; +import stream from 'stream'; import mime from 'mime-type/with-db'; +import Future from 'fibers/future'; + +Object.assign(FileUpload, { + handlers: {}, + + 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 }`]())); + }, + + defaultUploads() { + return { + collection: RocketChat.models.Uploads.model, + filter: new UploadFS.Filter({ + onCheck: FileUpload.validateFileUpload + }), + getPath(file) { + return `${ RocketChat.settings.get('uniqueID') }/uploads/${ file.rid }/${ file.userId }/${ file._id }`; + }, + // transformWrite: FileUpload.uploadsTransformWrite + onValidate: FileUpload.uploadsOnValidate + }; + }, + + defaultAvatars() { + return { + collection: RocketChat.models.Avatars.model, + // filter: new UploadFS.Filter({ + // onCheck: FileUpload.validateFileUpload + // }), + // transformWrite: FileUpload.avatarTransformWrite, + getPath(file) { + return `${ RocketChat.settings.get('uniqueID') }/avatars/${ file.userId }`; + }, + onValidate: FileUpload.avatarsOnValidate, + onFinishUpload: FileUpload.avatarsOnFinishUpload + }; + }, + + avatarTransformWrite(readStream, writeStream/*, fileId, file*/) { + if (RocketChatFile.enabled === false || RocketChat.settings.get('Accounts_AvatarResize') !== true) { + return readStream.pipe(writeStream); + } + const height = RocketChat.settings.get('Accounts_AvatarSize'); + const width = height; + return RocketChatFile.gm(readStream).background('#ffffff').resize(width, `${ height }^`).gravity('Center').crop(width, height).extent(width, height).stream('jpeg').pipe(writeStream); + }, + + avatarsOnValidate(file) { + if (RocketChatFile.enabled === false || RocketChat.settings.get('Accounts_AvatarResize') !== true) { + return; + } + + const tmpFile = UploadFS.getTempFilePath(file._id); + + const fut = new Future(); + + const height = RocketChat.settings.get('Accounts_AvatarSize'); + const width = height; + + RocketChatFile.gm(tmpFile).background('#ffffff').resize(width, `${ height }^`).gravity('Center').crop(width, height).extent(width, height).setFormat('jpeg').write(tmpFile, Meteor.bindEnvironment((err) => { + if (err != null) { + console.error(err); + } + + const size = fs.lstatSync(tmpFile).size; + this.getCollection().direct.update({_id: file._id}, {$set: {size}}); + fut.return(); + })); + + return fut.wait(); + }, + + uploadsTransformWrite(readStream, writeStream, fileId, file) { + if (RocketChatFile.enabled === false || !/^image\/.+/.test(file.type)) { + return readStream.pipe(writeStream); + } + + let stream = undefined; -FileUpload.handlers = {}; + const identify = function(err, data) { + if (err) { + return stream.pipe(writeStream); + } -FileUpload.addHandler = function(store, handler) { - this.handlers[store] = handler; -}; + file.identify = { + format: data.format, + size: data.size + }; -FileUpload.delete = function(fileId) { - const file = RocketChat.models.Uploads.findOneById(fileId); + if (data.Orientation && !['', 'Unknown', 'Undefined'].includes(data.Orientation)) { + RocketChatFile.gm(stream).autoOrient().stream().pipe(writeStream); + } else { + stream.pipe(writeStream); + } + }; - if (!file) { - return; + stream = RocketChatFile.gm(readStream).identify(identify).stream(); + }, + + uploadsOnValidate(file) { + if (RocketChatFile.enabled === false || !/^image\/((x-windows-)?bmp|p?jpeg|png)$/.test(file.type)) { + return; + } + + const tmpFile = UploadFS.getTempFilePath(file._id); + + const fut = new Future(); + + const identify = Meteor.bindEnvironment((err, data) => { + if (err != null) { + console.error(err); + return fut.return(); + } + + file.identify = { + format: data.format, + size: data.size + }; + + if ([null, undefined, '', 'Unknown', 'Undefined'].includes(data.Orientation)) { + return fut.return(); + } + + RocketChatFile.gm(tmpFile).autoOrient().write(tmpFile, Meteor.bindEnvironment((err) => { + if (err != null) { + console.error(err); + } + + const size = fs.lstatSync(tmpFile).size; + this.getCollection().direct.update({_id: file._id}, {$set: {size}}); + fut.return(); + })); + }); + + RocketChatFile.gm(tmpFile).identify(identify); + + return fut.wait(); + }, + + avatarsOnFinishUpload(file) { + // update file record to match user's username + const user = RocketChat.models.Users.findOneById(file.userId); + const oldAvatar = RocketChat.models.Avatars.findOneByName(user.username); + if (oldAvatar) { + RocketChat.models.Avatars.deleteFile(oldAvatar._id); + } + RocketChat.models.Avatars.updateFileNameById(file._id, user.username); + // console.log('upload finished ->', file); + }, + + addExtensionTo(file) { + if (mime.lookup(file.name) === file.type) { + return file; + } + + const ext = mime.extension(file.type); + if (ext && false === new RegExp(`\.${ ext }$`, 'i').test(file.name)) { + file.name = `${ file.name }.${ ext }`; + } + + return file; + }, + + getStore(modelName) { + const storageType = RocketChat.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) { + if (file.store && this.handlers && this.handlers[file.store] && this.handlers[file.store].get) { + this.handlers[file.store].get(file, req, res, next); + } else { + res.writeHead(404); + res.end(); + return; + } } +}); + - this.handlers[file.store].delete(file); +export class FileUploadClass { + constructor({ name, model, store, get, insert, getStore }) { + this.name = name; + this.model = model || this.getModelFromName(); + this._store = store || UploadFS.getStore(name); + this.get = get; - return RocketChat.models.Uploads.remove(file._id); -}; + if (insert) { + this.insert = insert; + } -FileUpload.get = function(file, req, res, next) { - if (file.store && this.handlers && this.handlers[file.store] && this.handlers[file.store].get) { - this.handlers[file.store].get.call(this, file, req, res, next); - } else { - res.writeHead(404); - res.end(); - return; + if (getStore) { + this.getStore = getStore; + } + + FileUpload.handlers[name] = this; } -}; -FileUpload.addExtensionTo = function(file) { - if (mime.lookup(file.name) === file.type) { - return file; + getStore() { + return this._store; + } + + get store() { + return this.getStore(); + } + + set store(store) { + this._store = store; + } + + getModelFromName() { + return RocketChat.models[this.name.split(':')[1]]; + } + + delete(fileId) { + if (this.store && this.store.delete) { + this.store.delete(fileId); + } + + return this.model.deleteFile(fileId); } - const ext = mime.extension(file.type); - if (ext && false === new RegExp(`\.${ ext }$`, 'i').test(file.name)) { - file.name = `${ file.name }.${ ext }`; + deleteById(fileId) { + const file = this.model.findOneById(fileId); + + if (!file) { + return; + } + + const store = FileUpload.getStoreByName(file.store); + + return store.delete(file._id); } - return file; -}; + deleteByName(fileName) { + const file = this.model.findOneByName(fileName); + + if (!file) { + return; + } + + const store = FileUpload.getStoreByName(file.store); + + return store.delete(file._id); + } + + insert(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; + } + } + } +} diff --git a/packages/rocketchat-file-upload/server/methods/getS3FileUrl.js b/packages/rocketchat-file-upload/server/methods/getS3FileUrl.js index c3cbca844eb8..421740d18b84 100644 --- a/packages/rocketchat-file-upload/server/methods/getS3FileUrl.js +++ b/packages/rocketchat-file-upload/server/methods/getS3FileUrl.js @@ -1,26 +1,11 @@ -import AWS4 from '../lib/AWS4.js'; +/* globals UploadFS */ let protectedFiles; -let S3accessKey; -let S3secretKey; -let S3expiryTimeSpan; RocketChat.settings.get('FileUpload_ProtectFiles', function(key, value) { protectedFiles = value; }); -RocketChat.settings.get('FileUpload_S3_AWSAccessKeyId', function(key, value) { - S3accessKey = value; -}); - -RocketChat.settings.get('FileUpload_S3_AWSSecretAccessKey', function(key, value) { - S3secretKey = value; -}); - -RocketChat.settings.get('FileUpload_S3_URLExpiryTimeSpan', function(key, value) { - S3expiryTimeSpan = value; -}); - Meteor.methods({ getS3FileUrl(fileId) { if (protectedFiles && !Meteor.userId()) { @@ -28,21 +13,6 @@ Meteor.methods({ } const file = RocketChat.models.Uploads.findOneById(fileId); - const credential = { - accessKeyId: S3accessKey, - secretKey: S3secretKey - }; - - const req = { - bucket: file.s3.bucket, - region: file.s3.region, - path: `/${ file.s3.path }${ file._id }`, - url: file.url, - expire: Math.max(5, S3expiryTimeSpan) - }; - - const queryString = AWS4.sign(req, credential); - - return `${ file.url }?${ queryString }`; + return UploadFS.getStore('AmazonS3:Uploads').getRedirectURL(file); } }); diff --git a/packages/rocketchat-file-upload/ufs/AmazonS3/client.js b/packages/rocketchat-file-upload/ufs/AmazonS3/client.js new file mode 100644 index 000000000000..6d9f66f467a4 --- /dev/null +++ b/packages/rocketchat-file-upload/ufs/AmazonS3/client.js @@ -0,0 +1,6 @@ +import {UploadFS} from 'meteor/jalik:ufs'; + +export class AmazonS3Store extends UploadFS.Store {} + +// Add store to UFS namespace +UploadFS.store.AmazonS3 = AmazonS3Store; diff --git a/packages/rocketchat-file-upload/ufs/AmazonS3/server.js b/packages/rocketchat-file-upload/ufs/AmazonS3/server.js new file mode 100644 index 000000000000..f69b05a883c4 --- /dev/null +++ b/packages/rocketchat-file-upload/ufs/AmazonS3/server.js @@ -0,0 +1,156 @@ +import {_} from 'meteor/underscore'; +import {UploadFS} from 'meteor/jalik:ufs'; +import S3 from 'aws-sdk/clients/s3'; +import stream from 'stream'; + +/** + * AmazonS3 store + * @param options + * @constructor + */ +export class AmazonS3Store extends UploadFS.Store { + + constructor(options) { + // Default options + // options.secretAccessKey, + // options.accessKeyId, + // options.region, + // options.sslEnabled // optional + + options = _.extend({ + httpOptions: { + timeout: 6000, + agent: false + } + }, options); + + super(options); + + const classOptions = options; + + const s3 = new S3(options.connection); + + options.getPath = options.getPath || function(file) { + return file._id; + }; + + this.getPath = function(file) { + if (file.AmazonS3) { + return file.AmazonS3.path; + } + // Compatibility + // TODO: Migration + if (file.s3) { + return file.s3.path + file._id; + } + }; + + this.getRedirectURL = function(file) { + const params = { + Key: this.getPath(file), + Expires: classOptions.URLExpiryTimeSpan + }; + + return s3.getSignedUrl('getObject', params); + }; + + /** + * Creates the file in the collection + * @param file + * @param callback + * @return {string} + */ + this.create = function(file, callback) { + check(file, Object); + + if (file._id == null) { + file._id = Random.id(); + } + + file.AmazonS3 = { + path: this.options.getPath(file) + }; + + file.store = this.options.name; // assign store to file + return this.getCollection().insert(file, callback); + }; + + /** + * Removes the file + * @param fileId + * @param callback + */ + this.delete = function(fileId, callback) { + const file = this.getCollection().findOne({_id: fileId}); + const params = { + Key: this.getPath(file) + }; + + s3.deleteObject(params, (err, data) => { + if (err) { + console.error(err); + } + + callback && callback(err, data); + }); + }; + + /** + * Returns the file read stream + * @param fileId + * @param file + * @param options + * @return {*} + */ + this.getReadStream = function(fileId, file, options = {}) { + const params = { + Key: this.getPath(file) + }; + + if (options.start && options.end) { + params.Range = `${ options.start } - ${ options.end }`; + } + + return s3.getObject(params).createReadStream(); + }; + + /** + * Returns the file write stream + * @param fileId + * @param file + * @param options + * @return {*} + */ + this.getWriteStream = function(fileId, file/*, options*/) { + const writeStream = new stream.PassThrough(); + writeStream.length = file.size; + + writeStream.on('newListener', (event, listener) => { + if (event === 'finish') { + process.nextTick(() => { + writeStream.removeListener(event, listener); + writeStream.on('real_finish', listener); + }); + } + }); + + s3.putObject({ + Key: this.getPath(file), + Body: writeStream, + ContentType: file.type + + }, (error) => { + if (error) { + console.error(error); + } + + writeStream.emit('real_finish'); + }); + + return writeStream; + }; + } +} + +// Add store to UFS namespace +UploadFS.store.AmazonS3 = AmazonS3Store; diff --git a/packages/rocketchat-file-upload/ufs/GoogleStorage/client.js b/packages/rocketchat-file-upload/ufs/GoogleStorage/client.js new file mode 100644 index 000000000000..2c11dc0d6828 --- /dev/null +++ b/packages/rocketchat-file-upload/ufs/GoogleStorage/client.js @@ -0,0 +1,6 @@ +import {UploadFS} from 'meteor/jalik:ufs'; + +export class GoogleStorageStore extends UploadFS.Store {} + +// Add store to UFS namespace +UploadFS.store.GoogleStorage = GoogleStorageStore; diff --git a/packages/rocketchat-file-upload/ufs/GoogleStorage/server.js b/packages/rocketchat-file-upload/ufs/GoogleStorage/server.js new file mode 100644 index 000000000000..8a764caac732 --- /dev/null +++ b/packages/rocketchat-file-upload/ufs/GoogleStorage/server.js @@ -0,0 +1,123 @@ +import {UploadFS} from 'meteor/jalik:ufs'; +import gcStorage from '@google-cloud/storage'; + +/** + * GoogleStorage store + * @param options + * @constructor + */ +export class GoogleStorageStore extends UploadFS.Store { + + constructor(options) { + super(options); + + const gcs = gcStorage(options.connection); + this.bucket = gcs.bucket(options.bucket); + + options.getPath = options.getPath || function(file) { + return file._id; + }; + + this.getPath = function(file) { + if (file.GoogleStorage) { + return file.GoogleStorage.path; + } + // Compatibility + // TODO: Migration + if (file.googleCloudStorage) { + return file.googleCloudStorage.path + file._id; + } + }; + + this.getRedirectURL = function(file, callback) { + const params = { + action: 'read', + responseDisposition: 'inline', + expires: Date.now()+this.options.URLExpiryTimeSpan*1000 + }; + + this.bucket.file(this.getPath(file)).getSignedUrl(params, callback); + }; + + /** + * Creates the file in the collection + * @param file + * @param callback + * @return {string} + */ + this.create = function(file, callback) { + check(file, Object); + + if (file._id == null) { + file._id = Random.id(); + } + + file.GoogleStorage = { + path: this.options.getPath(file) + }; + + file.store = this.options.name; // assign store to file + return this.getCollection().insert(file, callback); + }; + + /** + * Removes the file + * @param fileId + * @param callback + */ + this.delete = function(fileId, callback) { + const file = this.getCollection().findOne({_id: fileId}); + this.bucket.file(this.getPath(file)).delete(function(err, data) { + if (err) { + console.error(err); + } + + callback && callback(err, data); + }); + }; + + /** + * Returns the file read stream + * @param fileId + * @param file + * @param options + * @return {*} + */ + this.getReadStream = function(fileId, file, options = {}) { + const config = {}; + + if (options.start != null) { + config.start = options.start; + } + + if (options.end != null) { + config.end = options.end; + } + + return this.bucket.file(this.getPath(file)).createReadStream(config); + }; + + /** + * Returns the file write stream + * @param fileId + * @param file + * @param options + * @return {*} + */ + this.getWriteStream = function(fileId, file/*, options*/) { + return this.bucket.file(this.getPath(file)).createWriteStream({ + gzip: false, + metadata: { + contentType: file.type, + contentDisposition: `inline; filename=${ file.name }` + // metadata: { + // custom: 'metadata' + // } + } + }); + }; + } +} + +// Add store to UFS namespace +UploadFS.store.GoogleStorage = GoogleStorageStore; diff --git a/packages/rocketchat-i18n/i18n/ar.i18n.json b/packages/rocketchat-i18n/i18n/ar.i18n.json index 3dbd8af238ac..5e4f6947acbb 100644 --- a/packages/rocketchat-i18n/i18n/ar.i18n.json +++ b/packages/rocketchat-i18n/i18n/ar.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "السماح بتعديل الملف الشخصي للعضو", "Accounts_AvatarResize": "تغيير حجم الصور الرمزية", "Accounts_AvatarSize": "حجم الصورة الرمزية", - "Accounts_AvatarStorePath": "مسار تخزين الصورة الرمزية", - "Accounts_AvatarStoreType": "نوع تخزين الصورة الرمزية", "Accounts_BlockedDomainsList": "محظور قائمة المجالات", "Accounts_BlockedDomainsList_Description": "قائمة مفصولة بفواصل من المجالات سدت", "Accounts_BlockedUsernameList": "قائمة اﻷسماء المحظورة", diff --git a/packages/rocketchat-i18n/i18n/ca.i18n.json b/packages/rocketchat-i18n/i18n/ca.i18n.json index 185c793f6340..d9b22df36000 100644 --- a/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -29,8 +29,6 @@ "Accounts_AllowUserProfileChange": "Permetre modificar el perfil d'usuari", "Accounts_AvatarResize": "Canviar la mida dels avatars", "Accounts_AvatarSize": "Mida d'avatar", - "Accounts_AvatarStorePath": "Ruta d'emmagatzematge dels avatars", - "Accounts_AvatarStoreType": "Tipus d'emmagatzematge dels avatars", "Accounts_BlockedDomainsList": "Llista de dominis bloquejats", "Accounts_BlockedDomainsList_Description": "Llista de dominis bloquejats separada per comes", "Accounts_BlockedUsernameList": "Llista de noms d'usuari bloquejats", diff --git a/packages/rocketchat-i18n/i18n/cs.i18n.json b/packages/rocketchat-i18n/i18n/cs.i18n.json index 444538b8e3d7..2c48a17fa984 100644 --- a/packages/rocketchat-i18n/i18n/cs.i18n.json +++ b/packages/rocketchat-i18n/i18n/cs.i18n.json @@ -29,8 +29,6 @@ "Accounts_AllowUserProfileChange": "Povolit úpravy profilu", "Accounts_AvatarResize": "Rozměry avataru", "Accounts_AvatarSize": "Velikost avataru", - "Accounts_AvatarStorePath": "Cesta k úložišti avatarů", - "Accounts_AvatarStoreType": "Typ úložiště avatarů", "Accounts_BlockedDomainsList": "Seznam blokovaných domén", "Accounts_BlockedDomainsList_Description": "Čárkami oddělený seznam blokovaných domén", "Accounts_BlockedUsernameList": "Zakázaná uživatelská jména", diff --git a/packages/rocketchat-i18n/i18n/de-AT.i18n.json b/packages/rocketchat-i18n/i18n/de-AT.i18n.json index 0eedef43bf75..692513c0e910 100644 --- a/packages/rocketchat-i18n/i18n/de-AT.i18n.json +++ b/packages/rocketchat-i18n/i18n/de-AT.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "Benutzern das Ändern des Profils erlauben", "Accounts_AvatarResize": "Größe des Profilbilds anpassen", "Accounts_AvatarSize": "Größe des Profilbilds", - "Accounts_AvatarStorePath": "Speicherpfad des Profilbilds", - "Accounts_AvatarStoreType": "Speichertyp des Profilbilds", "Accounts_BlockedDomainsList": "Liste geblockter Domains", "Accounts_BlockedDomainsList_Description": "Kommata getrennte Liste von geblockten Domains", "Accounts_BlockedUsernameList": "Liste gesperrter Benutzernamen", diff --git a/packages/rocketchat-i18n/i18n/de.i18n.json b/packages/rocketchat-i18n/i18n/de.i18n.json index 1319011500de..545e16b1e447 100644 --- a/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/packages/rocketchat-i18n/i18n/de.i18n.json @@ -29,8 +29,6 @@ "Accounts_AllowUserProfileChange": "Benutzern das Ändern des Profils erlauben", "Accounts_AvatarResize": "Größe des Profilbilds anpassen", "Accounts_AvatarSize": "Größe des Profilbilds", - "Accounts_AvatarStorePath": "Speicherpfad des Profilbilds", - "Accounts_AvatarStoreType": "Speichertyp des Profilbilds", "Accounts_BlockedDomainsList": "Liste geblockter Domains", "Accounts_BlockedDomainsList_Description": "Kommata getrennte Liste von geblockten Domains", "Accounts_BlockedUsernameList": "Liste gesperrter Benutzernamen", diff --git a/packages/rocketchat-i18n/i18n/el.i18n.json b/packages/rocketchat-i18n/i18n/el.i18n.json index d4046ac1362d..ba47065b0679 100644 --- a/packages/rocketchat-i18n/i18n/el.i18n.json +++ b/packages/rocketchat-i18n/i18n/el.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "Επιτρέψτε Προφίλ Χρήστη Αλλαγή", "Accounts_AvatarResize": "Αλλαγή μεγέθους Avatars", "Accounts_AvatarSize": "Avatar Μέγεθος", - "Accounts_AvatarStorePath": "Path Avatar αποθήκευσης", - "Accounts_AvatarStoreType": "Avatar Τύπος αποθήκευσης", "Accounts_BlockedDomainsList": "Λίστα αποκλεισμένων τομέων", "Accounts_BlockedDomainsList_Description": "Διαχωρισμένες με κόμμα λίστα των αποκλεισμένων περιοχών", "Accounts_BlockedUsernameList": "Λίστα αποκλεισμένων Όνομα Χρήστη", diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 036fdf20deaa..1b2888536b9a 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -29,8 +29,6 @@ "Accounts_AllowUserProfileChange": "Allow User Profile Change", "Accounts_AvatarResize": "Resize Avatars", "Accounts_AvatarSize": "Avatar Size", - "Accounts_AvatarStorePath": "Avatar Storage Path", - "Accounts_AvatarStoreType": "Avatar Storage Type", "Accounts_BlockedDomainsList": "Blocked Domains List", "Accounts_BlockedDomainsList_Description": "Comma-separated list of blocked domains", "Accounts_BlockedUsernameList": "Blocked Username List", diff --git a/packages/rocketchat-i18n/i18n/es.i18n.json b/packages/rocketchat-i18n/i18n/es.i18n.json index e5dceb0f4260..915652d16149 100644 --- a/packages/rocketchat-i18n/i18n/es.i18n.json +++ b/packages/rocketchat-i18n/i18n/es.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "Permitir al Usuario modificar su Perfil", "Accounts_AvatarResize": "Cambiar el Tamaño de los Avatars", "Accounts_AvatarSize": "Tamaño de Avatar", - "Accounts_AvatarStorePath": "Ruta de almacenamiento de los avatares", - "Accounts_AvatarStoreType": "Tipo de Almacenamiento de Avatar", "Accounts_BlockedDomainsList": "Lista de dominios bloqueados", "Accounts_BlockedDomainsList_Description": "Lista de dominios bloqueados separada por comas", "Accounts_BlockedUsernameList": "Lista de nombres de usuario bloqueados", diff --git a/packages/rocketchat-i18n/i18n/fa.i18n.json b/packages/rocketchat-i18n/i18n/fa.i18n.json index 040e0fbb6974..db2d3e05f9a7 100644 --- a/packages/rocketchat-i18n/i18n/fa.i18n.json +++ b/packages/rocketchat-i18n/i18n/fa.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "کاربر اجازه می دهد مشخصات تغییر", "Accounts_AvatarResize": "تغییر اندازه آواتار ها", "Accounts_AvatarSize": "آواتار حجم", - "Accounts_AvatarStorePath": "مسیر آواتار ذخیره سازی", - "Accounts_AvatarStoreType": "آواتار نوع ذخیره سازی", "Accounts_BlockedDomainsList": "مسدود شده فهرست دامنه", "Accounts_BlockedDomainsList_Description": "جدا شده با کاما از حوزه های مسدود شده", "Accounts_BlockedUsernameList": "مسدود شده نام کاربری", diff --git a/packages/rocketchat-i18n/i18n/fi.i18n.json b/packages/rocketchat-i18n/i18n/fi.i18n.json index 2b0f459d43b8..fbfdaa6bc411 100644 --- a/packages/rocketchat-i18n/i18n/fi.i18n.json +++ b/packages/rocketchat-i18n/i18n/fi.i18n.json @@ -26,8 +26,6 @@ "Accounts_AllowUserProfileChange": "Salli käyttäjän profiilin muutos", "Accounts_AvatarResize": "Muuta avatarien kokoa", "Accounts_AvatarSize": "Avatarin koko", - "Accounts_AvatarStorePath": "Avatarin tallennuspolku", - "Accounts_AvatarStoreType": "Avatarien tallennusmuoto", "Accounts_BlockedDomainsList": "Estettyjen verkkotunnusten lista", "Accounts_BlockedDomainsList_Description": "Pilkuilla eroteltu lista estetyistä verkkotunnuksista", "Accounts_BlockedUsernameList": "Estettyjen käyttäjätunnusten lista", diff --git a/packages/rocketchat-i18n/i18n/fr.i18n.json b/packages/rocketchat-i18n/i18n/fr.i18n.json index 96b29850cf85..e3944df550d2 100644 --- a/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -29,8 +29,6 @@ "Accounts_AllowUserProfileChange": "Autoriser la modification de profil", "Accounts_AvatarResize": "Redimensionner les avatars", "Accounts_AvatarSize": "Taille de l'avatar", - "Accounts_AvatarStorePath": "Chemin de stockage des avatars", - "Accounts_AvatarStoreType": "Type de stockage des avatars", "Accounts_BlockedDomainsList": "Liste des domaines bloqués", "Accounts_BlockedDomainsList_Description": "Liste de domaines bloqués, séparés par des virgules", "Accounts_BlockedUsernameList": "Liste des noms d'utilisateurs bloqués", diff --git a/packages/rocketchat-i18n/i18n/he.i18n.json b/packages/rocketchat-i18n/i18n/he.i18n.json index b912f90ea58d..3be918e8545b 100644 --- a/packages/rocketchat-i18n/i18n/he.i18n.json +++ b/packages/rocketchat-i18n/i18n/he.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "אפשר למשתמש לשנות את הפרופיל", "Accounts_AvatarResize": "שנה את גודל תמונת הפרופיל", "Accounts_AvatarSize": "גודל תמונת הפרופיל", - "Accounts_AvatarStorePath": "נתיב תמונת הפרופיל", - "Accounts_AvatarStoreType": "סוג אחסון תמונת פרופיל", "Accounts_BlockedDomainsList": "רשימת דומיינים חסומים", "Accounts_BlockedDomainsList_Description": "רשימה מופרדת בפסיקים של דומיינים חסומים", "Accounts_BlockedUsernameList": "רשימת משתמש חסום", diff --git a/packages/rocketchat-i18n/i18n/hr.i18n.json b/packages/rocketchat-i18n/i18n/hr.i18n.json index 83e9f1a937d0..56c2306b8c87 100644 --- a/packages/rocketchat-i18n/i18n/hr.i18n.json +++ b/packages/rocketchat-i18n/i18n/hr.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "Dopusti Promjenu Profila", "Accounts_AvatarResize": "Promjeni veličinu Avatara", "Accounts_AvatarSize": "Veličina Avatara", - "Accounts_AvatarStorePath": "Putanja do spremišta Avatara ", - "Accounts_AvatarStoreType": "Tip Spremišta Avatara", "Accounts_BlockedDomainsList": "Popis blokiranih domena", "Accounts_BlockedDomainsList_Description": "Popis blokiranih domena odvojenih zarezom", "Accounts_BlockedUsernameList": "Popis blokiranih korisničkih imena", diff --git a/packages/rocketchat-i18n/i18n/hu.i18n.json b/packages/rocketchat-i18n/i18n/hu.i18n.json index 93d556692db4..7a936671ff20 100644 --- a/packages/rocketchat-i18n/i18n/hu.i18n.json +++ b/packages/rocketchat-i18n/i18n/hu.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "Felhasználó módosíthatja profilját", "Accounts_AvatarResize": "Profilképek átméretezése", "Accounts_AvatarSize": "Profilkép mérete", - "Accounts_AvatarStorePath": "Profilkép tárolásának elérési útja", - "Accounts_AvatarStoreType": "Profilkép tárolásának típusa", "Accounts_BlockedDomainsList": "Blokkolt domainek listája", "Accounts_BlockedDomainsList_Description": "Blokkolt domain-ek listája (vesszővel elválasztva)", "Accounts_BlockedUsernameList": "Blokkolt felhasználónevek listája", diff --git a/packages/rocketchat-i18n/i18n/id.i18n.json b/packages/rocketchat-i18n/i18n/id.i18n.json index c41f59d8ca82..954bc6247aba 100644 --- a/packages/rocketchat-i18n/i18n/id.i18n.json +++ b/packages/rocketchat-i18n/i18n/id.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "Memungkinkan Profil Pengguna Ganti", "Accounts_AvatarResize": "Ubah Ukuran Avatar", "Accounts_AvatarSize": "Ukuran Avatar", - "Accounts_AvatarStorePath": "Lokasi Penyimpanan Avatar", - "Accounts_AvatarStoreType": "Tipe Penyimpanan Avatar", "Accounts_BlockedDomainsList": "Daftar Domain diblokir", "Accounts_BlockedDomainsList_Description": "Dipisahkan dengan koma daftar domain diblokir", "Accounts_BlockedUsernameList": "Diblokir Daftar Nama", diff --git a/packages/rocketchat-i18n/i18n/it.i18n.json b/packages/rocketchat-i18n/i18n/it.i18n.json index 06d62a2f4d7f..f7d280bdb339 100644 --- a/packages/rocketchat-i18n/i18n/it.i18n.json +++ b/packages/rocketchat-i18n/i18n/it.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "Consenti cambio profilo utente", "Accounts_AvatarResize": "Ridimensiona Avatar", "Accounts_AvatarSize": "Dimensione Avatar", - "Accounts_AvatarStorePath": "Percorso Avatar", - "Accounts_AvatarStoreType": "Tipo di archiviazione per gli Avatar", "Accounts_BlockedDomainsList": "Elenco domini bloccati", "Accounts_BlockedDomainsList_Description": "elenco separato da virgole dei domini bloccati", "Accounts_BlockedUsernameList": "Lista nomi utente bloccati", diff --git a/packages/rocketchat-i18n/i18n/ja.i18n.json b/packages/rocketchat-i18n/i18n/ja.i18n.json index d207cdff4f02..39155bf24657 100644 --- a/packages/rocketchat-i18n/i18n/ja.i18n.json +++ b/packages/rocketchat-i18n/i18n/ja.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "プロフィールの変更を許可する", "Accounts_AvatarResize": "アバターの大きさを変更する", "Accounts_AvatarSize": "アバターの大きさ", - "Accounts_AvatarStorePath": "アバターの保存先", - "Accounts_AvatarStoreType": "アバターの保存先ストレージ種類", "Accounts_BlockedDomainsList": "ブロックされたドメイン一覧", "Accounts_BlockedDomainsList_Description": "カンマ区切りのブロックされたドメイン一覧", "Accounts_BlockedUsernameList": "ブロックされたユーザー名の一覧", diff --git a/packages/rocketchat-i18n/i18n/km.i18n.json b/packages/rocketchat-i18n/i18n/km.i18n.json index 2374ff4d1139..5a6446815f25 100644 --- a/packages/rocketchat-i18n/i18n/km.i18n.json +++ b/packages/rocketchat-i18n/i18n/km.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "អនុញ្ញាតិអ្នកប្រើប្រាស់ប្តូរព័ត៌មានផ្ទាល់ខ្លួន", "Accounts_AvatarResize": "ប្តូ​រ​ទំហំ Avatar", "Accounts_AvatarSize": "ទំហំ Avatar", - "Accounts_AvatarStorePath": "ទីតាំងផ្ទុក Avatar", - "Accounts_AvatarStoreType": "ប្រភេទបន្ទុក Avatar", "Accounts_BlockedDomainsList": "បញ្ជីដែន", "Accounts_BlockedDomainsList_Description": "បញ្ជីបំបែកដោយសញ្ញាក្បៀសនៃដែនបានបិទ", "Accounts_BlockedUsernameList": "បញ្ជីឈ្មោះអ្នកប្រើ", diff --git a/packages/rocketchat-i18n/i18n/ko.i18n.json b/packages/rocketchat-i18n/i18n/ko.i18n.json index c02eee6cb01b..5d7f599dd39c 100644 --- a/packages/rocketchat-i18n/i18n/ko.i18n.json +++ b/packages/rocketchat-i18n/i18n/ko.i18n.json @@ -26,8 +26,6 @@ "Accounts_AllowUserProfileChange": "사용자 프로필 변경을 허용", "Accounts_AvatarResize": "아바타 크기 조정", "Accounts_AvatarSize": "아바타 크기", - "Accounts_AvatarStorePath": "아바타 저장 경로", - "Accounts_AvatarStoreType": "아바파 저장 타입", "Accounts_BlockedDomainsList": "차단된 도메인 목록", "Accounts_BlockedDomainsList_Description": "쉼표로 구문된 차단 도메인 리스트", "Accounts_BlockedUsernameList": "차단된 사용자 리스트", diff --git a/packages/rocketchat-i18n/i18n/ku.i18n.json b/packages/rocketchat-i18n/i18n/ku.i18n.json index ac6d31aede77..b4fd33eb3a3e 100644 --- a/packages/rocketchat-i18n/i18n/ku.i18n.json +++ b/packages/rocketchat-i18n/i18n/ku.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "Destûrê bide User Profile Change", "Accounts_AvatarResize": "resize Avatars", "Accounts_AvatarSize": "Avatar Size", - "Accounts_AvatarStorePath": "Path Avatar Storage", - "Accounts_AvatarStoreType": "Avatar Type Storage", "Accounts_BlockedDomainsList": "Astengkirin Lîsteya Domain", "Accounts_BlockedDomainsList_Description": "lîsteya bêhnok-cuda ji qada astengkirin", "Accounts_BlockedUsernameList": "Astengkirin Lîsteya Username", diff --git a/packages/rocketchat-i18n/i18n/lo.i18n.json b/packages/rocketchat-i18n/i18n/lo.i18n.json index 6c71bb90512d..158e1b799ce2 100644 --- a/packages/rocketchat-i18n/i18n/lo.i18n.json +++ b/packages/rocketchat-i18n/i18n/lo.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "ອະນຸຍາດໃຫ້ການປ່ຽນແປງຂໍ້ມູນຂອງຜູ້ໃຊ້", "Accounts_AvatarResize": "ປັບຂະຫນາດນົດ", "Accounts_AvatarSize": "Avatar ຂະຫນາດ", - "Accounts_AvatarStorePath": "ເສັ້ນທາງກ້າວສູ່ຮູບສ່ວນຕົວການເກັບຮັກສາ", - "Accounts_AvatarStoreType": "Avatar ປະເພດການເກັບຮັກສາ", "Accounts_BlockedDomainsList": "ສະກັດຊີ Domains", "Accounts_BlockedDomainsList_Description": "ບັນຊີລາຍຊື່ຈຸດ, ແຍກຂອງໂດເມນກັດ", "Accounts_BlockedUsernameList": "ບັນຊີ Username ກັດ", diff --git a/packages/rocketchat-i18n/i18n/ms-MY.i18n.json b/packages/rocketchat-i18n/i18n/ms-MY.i18n.json index dfb52d0a5e78..0aa1d7eaf490 100644 --- a/packages/rocketchat-i18n/i18n/ms-MY.i18n.json +++ b/packages/rocketchat-i18n/i18n/ms-MY.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "Benarkan Profil Pengguna Tukar", "Accounts_AvatarResize": "Saiz semula Avatars", "Accounts_AvatarSize": "avatar Saiz", - "Accounts_AvatarStorePath": "Avatar Storage Path", - "Accounts_AvatarStoreType": "Avatar Storage Jenis", "Accounts_BlockedDomainsList": "Senarai Domain Disekat", "Accounts_BlockedDomainsList_Description": "Senarai diasingkan koma bagi domain disekat", "Accounts_BlockedUsernameList": "Senarai Nama pengguna Disekat", diff --git a/packages/rocketchat-i18n/i18n/nl.i18n.json b/packages/rocketchat-i18n/i18n/nl.i18n.json index a175e83d6164..81f9d3fdeafb 100644 --- a/packages/rocketchat-i18n/i18n/nl.i18n.json +++ b/packages/rocketchat-i18n/i18n/nl.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "Sta wijzen van gebruikers profiel toe", "Accounts_AvatarResize": "Wijzig Avatar grootte", "Accounts_AvatarSize": "Avatar grootte", - "Accounts_AvatarStorePath": "Avatar opslag pad", - "Accounts_AvatarStoreType": "Avatar opslag type", "Accounts_BlockedDomainsList": "Geblokkeerde domeinen List", "Accounts_BlockedDomainsList_Description": "Door komma's gescheiden lijst van geblokkeerde domeinen", "Accounts_BlockedUsernameList": "Geblokkeerde Gebruikersnaam List", diff --git a/packages/rocketchat-i18n/i18n/pl.i18n.json b/packages/rocketchat-i18n/i18n/pl.i18n.json index 28492fd71c39..c93dd52f309e 100644 --- a/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -24,8 +24,6 @@ "Accounts_AllowUserProfileChange": "Pozwól na zmienianie profilów użytkowników", "Accounts_AvatarResize": "Zmiana rozmiaru avatarów", "Accounts_AvatarSize": "Rozmiar avataru", - "Accounts_AvatarStorePath": "Ścieżka przechowywania avatarów", - "Accounts_AvatarStoreType": "Rodzaj magazynu avatarów", "Accounts_BlockedDomainsList": "Lista zablokowanych domen", "Accounts_BlockedDomainsList_Description": "Oddzielonych przecinkami lista zablokowanych domen", "Accounts_BlockedUsernameList": "Lista zablokowanych użytkowników", diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 6261000c036a..f34f587d3c20 100644 --- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "Permitir que o usuário altere o perfil", "Accounts_AvatarResize": "Redimensionar Avatares", "Accounts_AvatarSize": "Tamanho do Avatar", - "Accounts_AvatarStorePath": "Caminho para armazenar Avatares", - "Accounts_AvatarStoreType": "Tipo de armazenamento de Avatares", "Accounts_BlockedDomainsList": "Lista de Domínios Bloqueados", "Accounts_BlockedDomainsList_Description": "Lista de domínios bloqueados, separados por vírgulas ", "Accounts_BlockedUsernameList": "Lista de nomes de usuário bloqueados", diff --git a/packages/rocketchat-i18n/i18n/pt.i18n.json b/packages/rocketchat-i18n/i18n/pt.i18n.json index 5e5098430a62..bcec56545ecb 100644 --- a/packages/rocketchat-i18n/i18n/pt.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt.i18n.json @@ -29,8 +29,6 @@ "Accounts_AllowUserProfileChange": "Permitir que o usuário altere o perfil", "Accounts_AvatarResize": "Redimensionar Avatares", "Accounts_AvatarSize": "Tamanho do Avatar", - "Accounts_AvatarStorePath": "Caminho para armazenar Avatares", - "Accounts_AvatarStoreType": "Tipo de armazenamento de Avatares", "Accounts_BlockedDomainsList": "Lista de Domínios Bloqueados", "Accounts_BlockedDomainsList_Description": "Lista de domínios bloqueados, separados por vírgulas ", "Accounts_BlockedUsernameList": "Lista de nomes de usuário bloqueados", diff --git a/packages/rocketchat-i18n/i18n/ro.i18n.json b/packages/rocketchat-i18n/i18n/ro.i18n.json index 94f08574b0e6..1b9eba35a793 100644 --- a/packages/rocketchat-i18n/i18n/ro.i18n.json +++ b/packages/rocketchat-i18n/i18n/ro.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "Permite schimbarea profilului utilzatorilor", "Accounts_AvatarResize": "Redimensionarea Avatare", "Accounts_AvatarSize": "Dimensiune Avatar", - "Accounts_AvatarStorePath": "Calea de stocare Avatar", - "Accounts_AvatarStoreType": "Tip stocare Avatar", "Accounts_BlockedDomainsList": "Blocate List Domenii", "Accounts_BlockedDomainsList_Description": "Lista de elemente separate prin virgulă de domenii blocate", "Accounts_BlockedUsernameList": "Utilizator blocat Lista", diff --git a/packages/rocketchat-i18n/i18n/ru.i18n.json b/packages/rocketchat-i18n/i18n/ru.i18n.json index 36025fd5826a..57b3f42652df 100644 --- a/packages/rocketchat-i18n/i18n/ru.i18n.json +++ b/packages/rocketchat-i18n/i18n/ru.i18n.json @@ -28,8 +28,6 @@ "Accounts_AllowUserProfileChange": "Разрешить пользователю изменять настройки профиля", "Accounts_AvatarResize": "Изменение размера аватара", "Accounts_AvatarSize": "Размер аватара", - "Accounts_AvatarStorePath": "Путь к хранилищу аватаров", - "Accounts_AvatarStoreType": "Тип хранилища аватаров", "Accounts_BlockedDomainsList": "Список запрещённых доменов", "Accounts_BlockedDomainsList_Description": "Список запрещённых доменов, разделенных запятой", "Accounts_BlockedUsernameList": "Список заблокированных пользователей", diff --git a/packages/rocketchat-i18n/i18n/sq.i18n.json b/packages/rocketchat-i18n/i18n/sq.i18n.json index 2789b1173d7f..0da84a902bdd 100644 --- a/packages/rocketchat-i18n/i18n/sq.i18n.json +++ b/packages/rocketchat-i18n/i18n/sq.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "Lejo përdoruesit Profilin Change", "Accounts_AvatarResize": "Ndrysho madhësin e Avatarit", "Accounts_AvatarSize": "Madhësia e Avatarit", - "Accounts_AvatarStorePath": "Avatari Storage Path", - "Accounts_AvatarStoreType": "Avatari Storage Lloji", "Accounts_BlockedDomainsList": "Blocked Lista Domains", "Accounts_BlockedDomainsList_Description": "lista të ndara me presje të fushave të bllokuara", "Accounts_BlockedUsernameList": "Blocked List Emri i përdoruesit", diff --git a/packages/rocketchat-i18n/i18n/sr.i18n.json b/packages/rocketchat-i18n/i18n/sr.i18n.json index 86b6a9570fcb..7b658902e6de 100644 --- a/packages/rocketchat-i18n/i18n/sr.i18n.json +++ b/packages/rocketchat-i18n/i18n/sr.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "Дозволите Усер Профиле Цханге", "Accounts_AvatarResize": "ресизе Аватари", "Accounts_AvatarSize": "аватар величина", - "Accounts_AvatarStorePath": "Аватар складиштења Пут", - "Accounts_AvatarStoreType": "Аватар Стораге Типе:", "Accounts_BlockedDomainsList": "Блокиран доменов", "Accounts_BlockedDomainsList_Description": "Зарезом одвојена листа блокираних домена", "Accounts_BlockedUsernameList": "Блокиран име Списак", diff --git a/packages/rocketchat-i18n/i18n/sv.i18n.json b/packages/rocketchat-i18n/i18n/sv.i18n.json index b677c0687450..90899745d5b9 100644 --- a/packages/rocketchat-i18n/i18n/sv.i18n.json +++ b/packages/rocketchat-i18n/i18n/sv.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "Tillåt byte av användarprofil", "Accounts_AvatarResize": "Ändra storlek på avatarer", "Accounts_AvatarSize": "Storlek på avatar", - "Accounts_AvatarStorePath": "Sökväg till avatarförråd", - "Accounts_AvatarStoreType": "Typ av avatarförråd", "Accounts_BlockedDomainsList": "Lista över blockerade domäner", "Accounts_BlockedDomainsList_Description": "Kommaseparerad lista över blockerade domäner", "Accounts_BlockedUsernameList": "Användarlista över blockerade", diff --git a/packages/rocketchat-i18n/i18n/ta-IN.i18n.json b/packages/rocketchat-i18n/i18n/ta-IN.i18n.json index 58ea59cd2b62..9d857fdf3254 100644 --- a/packages/rocketchat-i18n/i18n/ta-IN.i18n.json +++ b/packages/rocketchat-i18n/i18n/ta-IN.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "பயனர் விவரம் மாற்றம் அனுமதி", "Accounts_AvatarResize": "அவதாரங்களை அளவை", "Accounts_AvatarSize": "avatar அளவு", - "Accounts_AvatarStorePath": "அவதார் சேமிப்பு பாதை", - "Accounts_AvatarStoreType": "அவதார் சேமிப்பு வகை", "Accounts_BlockedDomainsList": "தடுக்கப்பட்ட களங்கள் பட்டியல்", "Accounts_BlockedDomainsList_Description": "தடுக்கப்பட்டது களங்களின் கமாவால் பிரிக்கப்பட்ட பட்டியல்", "Accounts_BlockedUsernameList": "தடுக்கப்பட்ட பயனர் பெயர் பட்டியல்", diff --git a/packages/rocketchat-i18n/i18n/tr.i18n.json b/packages/rocketchat-i18n/i18n/tr.i18n.json index 1f577efdd77d..c70b29e1ba59 100644 --- a/packages/rocketchat-i18n/i18n/tr.i18n.json +++ b/packages/rocketchat-i18n/i18n/tr.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "Kullanıcının Profilini Değiştirmesine izin ver", "Accounts_AvatarResize": "Profil Resimlerini yeniden boyutlandır", "Accounts_AvatarSize": "Profil Resmi Boyutu", - "Accounts_AvatarStorePath": "Profil Resmi Depo Yolu", - "Accounts_AvatarStoreType": "Profil Resmi Depolama Türü", "Accounts_BlockedDomainsList": "Engellenen Alanlar Listesi", "Accounts_BlockedDomainsList_Description": "Engellenen alanların virgülle ayrılmış listesi", "Accounts_BlockedUsernameList": "Engellenen Kullanıcı Adı Listesi", diff --git a/packages/rocketchat-i18n/i18n/ug.i18n.json b/packages/rocketchat-i18n/i18n/ug.i18n.json index d3a1dc7c7cfa..ba53f4000c38 100644 --- a/packages/rocketchat-i18n/i18n/ug.i18n.json +++ b/packages/rocketchat-i18n/i18n/ug.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "ئاكونت ئەزانىڭ ماتېرىيالىنى ئۆزگەرتىشكە رۇخسەت قىلدى", "Accounts_AvatarResize": "ئاكونت باش سۈرئەتنىڭ چوڭ كىچىكلىكىنى تەڭشەش", "Accounts_AvatarSize": "ئاكونت باش سۈرئەتنىڭ چوڭ كىچىكلىكى", - "Accounts_AvatarStorePath": "ئاكونت باش سۈرئەت ساقلاش ئادرېسى", - "Accounts_AvatarStoreType": "ئاكونت باش سۈرئەت ساقلاش تىپى", "Accounts_BlockedDomainsList": "ئاكونت تور بەت نامى تىزىملىكىنى توسۇۋېتىلدى", "Accounts_BlockedDomainsList_Description": "چىكىتلىك پەش ئارقىلىق توسۇۋېتىلگەن توربەت نامى تىزىملىكى", "Accounts_BlockedUsernameList": "ئاكونت مەنئىي قىلغان ئەزا تىزىملىكى", diff --git a/packages/rocketchat-i18n/i18n/uk.i18n.json b/packages/rocketchat-i18n/i18n/uk.i18n.json index ff1c6365af7e..9cdb267deb27 100644 --- a/packages/rocketchat-i18n/i18n/uk.i18n.json +++ b/packages/rocketchat-i18n/i18n/uk.i18n.json @@ -25,8 +25,6 @@ "Accounts_AllowUserProfileChange": "Дозволити користувачеві змінювати налаштування профілю", "Accounts_AvatarResize": "Змінити розмір аватару", "Accounts_AvatarSize": "Розмір аватару", - "Accounts_AvatarStorePath": "Шлях до сховища з аватарами", - "Accounts_AvatarStoreType": "Тип сховища с аватарами", "Accounts_BlockedDomainsList": "Перелік заблокованих доменів", "Accounts_BlockedDomainsList_Description": "Перелік заблокованих доменів, розділених комою", "Accounts_BlockedUsernameList": "Перелік заблокованих користувачів", diff --git a/packages/rocketchat-i18n/i18n/zh-HK.i18n.json b/packages/rocketchat-i18n/i18n/zh-HK.i18n.json index 549746827755..e3365a6452c6 100644 --- a/packages/rocketchat-i18n/i18n/zh-HK.i18n.json +++ b/packages/rocketchat-i18n/i18n/zh-HK.i18n.json @@ -20,7 +20,6 @@ "Accounts_AllowUserProfileChange": "允許使用者變更個人檔案", "Accounts_AvatarResize": "調整頭像大\b小", "Accounts_AvatarSize": "頭像大\b小", - "Accounts_AvatarStorePath": "頭像儲存路徑", "Accounts_BlockedDomainsList": "黑名單網域列表", "Accounts_denyUnverifiedEmail": "拒绝未经验证的电子邮件", "Accounts_EmailVerification": "邮件验证", diff --git a/packages/rocketchat-i18n/i18n/zh-TW.i18n.json b/packages/rocketchat-i18n/i18n/zh-TW.i18n.json index d06f9788e46e..53c8db1dc545 100644 --- a/packages/rocketchat-i18n/i18n/zh-TW.i18n.json +++ b/packages/rocketchat-i18n/i18n/zh-TW.i18n.json @@ -25,8 +25,6 @@ "Accounts_AllowUserProfileChange": "允許變更使用者個人檔案", "Accounts_AvatarResize": "調整大頭貼尺寸", "Accounts_AvatarSize": "大頭貼尺寸", - "Accounts_AvatarStorePath": "大頭貼儲存路徑", - "Accounts_AvatarStoreType": "大頭貼儲存類型", "Accounts_BlockedDomainsList": "黑名單網域列表", "Accounts_BlockedDomainsList_Description": "以逗號分隔的網域黑名單", "Accounts_BlockedUsernameList": "使用者黑名單", diff --git a/packages/rocketchat-i18n/i18n/zh.i18n.json b/packages/rocketchat-i18n/i18n/zh.i18n.json index 00b01cda799a..4f1c6f29eb57 100644 --- a/packages/rocketchat-i18n/i18n/zh.i18n.json +++ b/packages/rocketchat-i18n/i18n/zh.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "允许修改个人资料", "Accounts_AvatarResize": "调整头像大小", "Accounts_AvatarSize": "头像大小", - "Accounts_AvatarStorePath": "头像存储路径", - "Accounts_AvatarStoreType": "头像存储类型", "Accounts_BlockedDomainsList": "已屏蔽域名列表", "Accounts_BlockedDomainsList_Description": "以逗号分隔的屏蔽的域名列表", "Accounts_BlockedUsernameList": "已屏蔽的用户名列表", diff --git a/packages/rocketchat-importer/server/classes/ImporterBase.js b/packages/rocketchat-importer/server/classes/ImporterBase.js index 9e3f98c4011c..a966c786f05f 100644 --- a/packages/rocketchat-importer/server/classes/ImporterBase.js +++ b/packages/rocketchat-importer/server/classes/ImporterBase.js @@ -190,59 +190,55 @@ Importer.Base = class Base { const requestModule = /https/i.test(fileUrl) ? Importer.Base.https : Importer.Base.http; return requestModule.get(fileUrl, Meteor.bindEnvironment(function(stream) { - const fileId = Meteor.fileStore.create(details); - if (fileId) { - return Meteor.fileStore.write(stream, fileId, function(err, file) { - if (err) { - throw new Error(err); - } else { - const url = file.url.replace(Meteor.absoluteUrl(), '/'); - - const attachment = { - title: `File Uploaded: ${ file.name }`, - title_link: url - }; - - if (/^image\/.+/.test(file.type)) { - attachment.image_url = url; - attachment.image_type = file.type; - attachment.image_size = file.size; - attachment.image_dimensions = file.identify != null ? file.identify.size : undefined; - } - - if (/^audio\/.+/.test(file.type)) { - attachment.audio_url = url; - attachment.audio_type = file.type; - attachment.audio_size = file.size; - } - - if (/^video\/.+/.test(file.type)) { - attachment.video_url = url; - attachment.video_type = file.type; - attachment.video_size = file.size; - } - - const msg = { - rid: details.rid, - ts: timeStamp, - msg: '', - file: { - _id: file._id - }, - groupable: false, - attachments: [attachment] - }; - - if ((details.message_id != null) && (typeof details.message_id === 'string')) { - msg['_id'] = details.message_id; - } - - return RocketChat.sendMessage(user, msg, room, true); + const fileStore = FileUpload.getStore('Uploads'); + fileStore.insert(details, stream, function(err, file) { + if (err) { + throw new Error(err); + } else { + const url = file.url.replace(Meteor.absoluteUrl(), '/'); + + const attachment = { + title: `File Uploaded: ${ file.name }`, + title_link: url + }; + + if (/^image\/.+/.test(file.type)) { + attachment.image_url = url; + attachment.image_type = file.type; + attachment.image_size = file.size; + attachment.image_dimensions = file.identify != null ? file.identify.size : undefined; } - }); - } else { - return this.logger.error(`Failed to create the store for ${ fileUrl }!!!`); - } + + if (/^audio\/.+/.test(file.type)) { + attachment.audio_url = url; + attachment.audio_type = file.type; + attachment.audio_size = file.size; + } + + if (/^video\/.+/.test(file.type)) { + attachment.video_url = url; + attachment.video_type = file.type; + attachment.video_size = file.size; + } + + const msg = { + rid: details.rid, + ts: timeStamp, + msg: '', + file: { + _id: file._id + }, + groupable: false, + attachments: [attachment] + }; + + if ((details.message_id != null) && (typeof details.message_id === 'string')) { + msg['_id'] = details.message_id; + } + + return RocketChat.sendMessage(user, msg, room, true); + } + }); })); } }; diff --git a/packages/rocketchat-ldap/server/sync.js b/packages/rocketchat-ldap/server/sync.js index 06763c0a97e6..c8c944a145c1 100644 --- a/packages/rocketchat-ldap/server/sync.js +++ b/packages/rocketchat-ldap/server/sync.js @@ -172,16 +172,22 @@ syncUserData = function syncUserData(user, ldapUser) { const avatar = ldapUser.raw.thumbnailPhoto || ldapUser.raw.jpegPhoto; if (avatar) { logger.info('Syncing user avatar'); + const rs = RocketChatFile.bufferToStream(avatar); - RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ user.username }.jpg`)); - const ws = RocketChatFileAvatarInstance.createWriteStream(encodeURIComponent(`${ user.username }.jpg`), 'image/jpeg'); - ws.on('end', Meteor.bindEnvironment(function() { + const fileStore = FileUpload.getStore('Avatars'); + fileStore.deleteByName(user.username); + + const file = { + userId: user._id, + type: 'image/jpeg' + }; + + fileStore.insert(file, rs, () => { Meteor.setTimeout(function() { RocketChat.models.Users.setAvatarOrigin(user._id, 'ldap'); RocketChat.Notifications.notifyLogged('updateAvatar', {username: user.username}); }, 500); - })); - rs.pipe(ws); + }); } } }; diff --git a/packages/rocketchat-lib/client/models/Avatars.js b/packages/rocketchat-lib/client/models/Avatars.js new file mode 100644 index 000000000000..bd8804e746c8 --- /dev/null +++ b/packages/rocketchat-lib/client/models/Avatars.js @@ -0,0 +1,6 @@ +RocketChat.models.Avatars = new class extends RocketChat.models._Base { + constructor() { + super(); + this._initModel('avatars'); + } +}; diff --git a/packages/rocketchat-lib/package.js b/packages/rocketchat-lib/package.js index 3042469d88fa..f5df7120546e 100644 --- a/packages/rocketchat-lib/package.js +++ b/packages/rocketchat-lib/package.js @@ -103,6 +103,7 @@ Package.onUse(function(api) { // SERVER MODELS api.addFiles('server/models/_Base.js', 'server'); + api.addFiles('server/models/Avatars.js', 'server'); api.addFiles('server/models/Messages.js', 'server'); api.addFiles('server/models/Reports.js', 'server'); api.addFiles('server/models/Rooms.js', 'server'); @@ -196,6 +197,7 @@ Package.onUse(function(api) { // CLIENT MODELS api.addFiles('client/models/_Base.js', 'client'); + api.addFiles('client/models/Avatars.js', 'client'); api.addFiles('client/models/Uploads.js', 'client'); // CLIENT VIEWS diff --git a/packages/rocketchat-lib/server/functions/deleteMessage.js b/packages/rocketchat-lib/server/functions/deleteMessage.js index 7a190964be11..50fd5999864e 100644 --- a/packages/rocketchat-lib/server/functions/deleteMessage.js +++ b/packages/rocketchat-lib/server/functions/deleteMessage.js @@ -21,7 +21,7 @@ RocketChat.deleteMessage = function(message, user) { } if (message.file && message.file._id) { - FileUpload.delete(message.file._id); + FileUpload.getStore('Uploads').deleteById(message.file._id); } Meteor.defer(function() { diff --git a/packages/rocketchat-lib/server/functions/deleteUser.js b/packages/rocketchat-lib/server/functions/deleteUser.js index a139389b064d..ab7ec6719b94 100644 --- a/packages/rocketchat-lib/server/functions/deleteUser.js +++ b/packages/rocketchat-lib/server/functions/deleteUser.js @@ -1,4 +1,3 @@ -/* globals RocketChat */ RocketChat.deleteUser = function(userId) { const user = RocketChat.models.Users.findOneById(userId); @@ -22,7 +21,7 @@ RocketChat.deleteUser = function(userId) { // removes user's avatar if (user.avatarOrigin === 'upload' || user.avatarOrigin === 'url') { - RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ user.username }.jpg`)); + FileUpload.getStore('Avatars').deleteByName(user.username); } RocketChat.models.Integrations.disableByUserId(userId); // Disables all the integrations which rely on the user being deleted. diff --git a/packages/rocketchat-lib/server/functions/setUserAvatar.js b/packages/rocketchat-lib/server/functions/setUserAvatar.js index 5b005313bb6c..480688d7904b 100644 --- a/packages/rocketchat-lib/server/functions/setUserAvatar.js +++ b/packages/rocketchat-lib/server/functions/setUserAvatar.js @@ -39,14 +39,20 @@ RocketChat.setUserAvatar = function(user, dataURI, contentType, service) { contentType = fileData.contentType; } - const rs = RocketChatFile.bufferToStream(new Buffer(image, encoding)); - RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ user.username }.jpg`)); - const ws = RocketChatFileAvatarInstance.createWriteStream(encodeURIComponent(`${ user.username }.jpg`), contentType); - ws.on('end', Meteor.bindEnvironment(function() { + const buffer = new Buffer(image, encoding); + const fileStore = FileUpload.getStore('Avatars'); + fileStore.deleteByName(user.username); + + const file = { + userId: user._id, + type: contentType, + size: buffer.length + }; + + fileStore.insert(file, buffer, () => { Meteor.setTimeout(function() { RocketChat.models.Users.setAvatarOrigin(user._id, service); RocketChat.Notifications.notifyLogged('updateAvatar', {username: user.username}); }, 500); - })); - rs.pipe(ws); + }); }; diff --git a/packages/rocketchat-lib/server/functions/setUsername.js b/packages/rocketchat-lib/server/functions/setUsername.js index 455e6cfbc67c..20eeabdccf0f 100644 --- a/packages/rocketchat-lib/server/functions/setUsername.js +++ b/packages/rocketchat-lib/server/functions/setUsername.js @@ -65,12 +65,11 @@ RocketChat._setUsername = function(userId, u) { RocketChat.models.Rooms.replaceUsernameOfUserByUserId(user._id, username); RocketChat.models.Subscriptions.setUserUsernameByUserId(user._id, username); RocketChat.models.Subscriptions.setNameForDirectRoomsWithOldName(previousUsername, username); - const rs = RocketChatFileAvatarInstance.getFileWithReadStream(encodeURIComponent(`${ previousUsername }.jpg`)); - if (rs != null) { - RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ username }.jpg`)); - const ws = RocketChatFileAvatarInstance.createWriteStream(encodeURIComponent(`${ username }.jpg`), rs.contentType); - ws.on('end', Meteor.bindEnvironment(() => RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ previousUsername }.jpg`)))); - rs.readStream.pipe(ws); + + const fileStore = FileUpload.getStore('Avatars'); + const file = fileStore.model.findOneByName(previousUsername); + if (file) { + fileStore.model.updateFileNameById(file._id, username); } } // Set new username* diff --git a/packages/rocketchat-lib/server/models/Avatars.js b/packages/rocketchat-lib/server/models/Avatars.js new file mode 100644 index 000000000000..43c7b6be6af6 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Avatars.js @@ -0,0 +1,105 @@ +RocketChat.models.Avatars = new class extends RocketChat.models._Base { + constructor() { + super('avatars'); + + this.tryEnsureIndex({ name: 1 }); + } + + insertAvatarFileInit(name, userId, store, file, extra) { + const fileData = { + _id: name, + name, + userId, + store, + complete: false, + uploading: true, + progress: 0, + extension: s.strRightBack(file.name, '.'), + uploadedAt: new Date() + }; + + _.extend(fileData, file, extra); + + return this.insertOrUpsert(fileData); + } + + updateFileComplete(fileId, userId, file) { + if (!fileId) { + return; + } + + const filter = { + _id: fileId, + userId + }; + + const update = { + $set: { + complete: true, + uploading: false, + progress: 1 + } + }; + + update.$set = _.extend(file, update.$set); + + if (this.model.direct && this.model.direct.update) { + return this.model.direct.update(filter, update); + } else { + return this.update(filter, update); + } + } + + findOneByName(name) { + return this.findOne({ name }); + } + + updateFileNameById(fileId, name) { + const filter = { _id: fileId }; + const update = { + $set: { + name + } + }; + if (this.model.direct && this.model.direct.update) { + return this.model.direct.update(filter, update); + } else { + return this.update(filter, update); + } + } + + // @TODO deprecated + updateFileCompleteByNameAndUserId(name, userId, url) { + if (!name) { + return; + } + + const filter = { + name, + userId + }; + + const update = { + $set: { + complete: true, + uploading: false, + progress: 1, + url + } + }; + + if (this.model.direct && this.model.direct.update) { + return this.model.direct.update(filter, update); + } else { + return this.update(filter, update); + } + } + + deleteFile(fileId) { + if (this.model.direct && this.model.direct.remove) { + return this.model.direct.remove({ _id: fileId }); + } else { + return this.remove({ _id: fileId }); + } + } +}; diff --git a/packages/rocketchat-lib/server/models/Uploads.js b/packages/rocketchat-lib/server/models/Uploads.js index 252d48fb8782..3beaec043b8e 100644 --- a/packages/rocketchat-lib/server/models/Uploads.js +++ b/packages/rocketchat-lib/server/models/Uploads.js @@ -36,9 +36,8 @@ RocketChat.models.Uploads = new class extends RocketChat.models._Base { return this.find(fileQuery, fileOptions); } - insertFileInit(roomId, userId, store, file, extra) { + insertFileInit(userId, store, file, extra) { const fileData = { - rid: roomId, userId, store, complete: false, @@ -50,7 +49,7 @@ RocketChat.models.Uploads = new class extends RocketChat.models._Base { _.extend(fileData, file, extra); - if ((this.model.direct != null ? this.model.direct.insert : undefined) != null) { + if (this.model.direct && this.model.direct.insert != null) { file = this.model.direct.insert(fileData); } else { file = this.insert(fileData); @@ -80,7 +79,7 @@ RocketChat.models.Uploads = new class extends RocketChat.models._Base { update.$set = _.extend(file, update.$set); - if ((this.model.direct != null ? this.model.direct.insert : undefined) != null) { + if (this.model.direct && this.model.direct.update != null) { result = this.model.direct.update(filter, update); } else { result = this.update(filter, update); @@ -88,4 +87,12 @@ RocketChat.models.Uploads = new class extends RocketChat.models._Base { return result; } + + deleteFile(fileId) { + if (this.model.direct && this.model.direct.remove != null) { + return this.model.direct.remove({ _id: fileId }); + } else { + return this.remove({ _id: fileId }); + } + } }; diff --git a/packages/rocketchat-lib/server/startup/settings.js b/packages/rocketchat-lib/server/startup/settings.js index e62787932de0..1671d5185bee 100644 --- a/packages/rocketchat-lib/server/startup/settings.js +++ b/packages/rocketchat-lib/server/startup/settings.js @@ -169,25 +169,7 @@ RocketChat.settings.addGroup('Accounts', function() { value: true } }); - this.add('Accounts_AvatarStoreType', 'GridFS', { - type: 'select', - values: [ - { - key: 'GridFS', - i18nLabel: 'GridFS' - }, { - key: 'FileSystem', - i18nLabel: 'FileSystem' - } - ] - }); - this.add('Accounts_AvatarStorePath', '', { - type: 'string', - enableQuery: { - _id: 'Accounts_AvatarStoreType', - value: 'FileSystem' - } - }); + return this.add('Accounts_SetDefaultAvatar', true, { type: 'boolean' }); diff --git a/packages/rocketchat-slackbridge/slackbridge.js b/packages/rocketchat-slackbridge/slackbridge.js index b4bfa1686deb..e48446794082 100644 --- a/packages/rocketchat-slackbridge/slackbridge.js +++ b/packages/rocketchat-slackbridge/slackbridge.js @@ -592,58 +592,57 @@ class SlackBridge { const parsedUrl = url.parse(slackFileURL, true); parsedUrl.headers = { 'Authorization': `Bearer ${ this.apiToken }` }; requestModule.get(parsedUrl, Meteor.bindEnvironment((stream) => { - const fileId = Meteor.fileStore.create(details); - if (fileId) { - Meteor.fileStore.write(stream, fileId, (err, file) => { - if (err) { - throw new Error(err); - } else { - const url = file.url.replace(Meteor.absoluteUrl(), '/'); - const attachment = { - title: `File Uploaded: ${ file.name }`, - title_link: url - }; - - if (/^image\/.+/.test(file.type)) { - attachment.image_url = url; - attachment.image_type = file.type; - attachment.image_size = file.size; - attachment.image_dimensions = file.identify && file.identify.size; - } - if (/^audio\/.+/.test(file.type)) { - attachment.audio_url = url; - attachment.audio_type = file.type; - attachment.audio_size = file.size; - } - if (/^video\/.+/.test(file.type)) { - attachment.video_url = url; - attachment.video_type = file.type; - attachment.video_size = file.size; - } + const fileStore = FileUpload.getStore('Uploads'); - const msg = { - rid: details.rid, - ts: timeStamp, - msg: '', - file: { - _id: file._id - }, - groupable: false, - attachments: [attachment] - }; + fileStore.insert(details, stream, (err, file) => { + if (err) { + throw new Error(err); + } else { + const url = file.url.replace(Meteor.absoluteUrl(), '/'); + const attachment = { + title: `File Uploaded: ${ file.name }`, + title_link: url + }; - if (isImporting) { - msg.imported = 'slackbridge'; - } + if (/^image\/.+/.test(file.type)) { + attachment.image_url = url; + attachment.image_type = file.type; + attachment.image_size = file.size; + attachment.image_dimensions = file.identify && file.identify.size; + } + if (/^audio\/.+/.test(file.type)) { + attachment.audio_url = url; + attachment.audio_type = file.type; + attachment.audio_size = file.size; + } + if (/^video\/.+/.test(file.type)) { + attachment.video_url = url; + attachment.video_type = file.type; + attachment.video_size = file.size; + } - if (details.message_id && (typeof details.message_id === 'string')) { - msg['_id'] = details.message_id; - } + const msg = { + rid: details.rid, + ts: timeStamp, + msg: '', + file: { + _id: file._id + }, + groupable: false, + attachments: [attachment] + }; - return RocketChat.sendMessage(rocketUser, msg, rocketChannel, true); + if (isImporting) { + msg.imported = 'slackbridge'; } - }); - } + + if (details.message_id && (typeof details.message_id === 'string')) { + msg['_id'] = details.message_id; + } + + return RocketChat.sendMessage(rocketUser, msg, rocketChannel, true); + } + }); })); } diff --git a/packages/rocketchat-ui-account/client/avatar/prompt.js b/packages/rocketchat-ui-account/client/avatar/prompt.js index b2a02bb5b885..6fdd1bdcc5f3 100644 --- a/packages/rocketchat-ui-account/client/avatar/prompt.js +++ b/packages/rocketchat-ui-account/client/avatar/prompt.js @@ -1,4 +1,8 @@ +/* globals fileUploadHandler */ + import toastr from 'toastr'; +import mime from 'mime-type/with-db'; + Template.avatarPrompt.onCreated(function() { const self = this; self.suggestions = new ReactiveVar; @@ -44,10 +48,10 @@ Template.avatarPrompt.helpers({ }); Template.avatarPrompt.events({ - 'click .select-service'() { + 'click .select-service'(event, instance) { if (this.service === 'initials') { Meteor.call('resetAvatar', function(err) { - if (err && err.details.timeToReset && err.details.timeToReset) { + if (err && err.details && err.details.timeToReset) { toastr.error(t('error-too-many-requests', { seconds: parseInt(err.details.timeToReset / 1000) })); @@ -75,10 +79,38 @@ Template.avatarPrompt.events({ } else { toastr.error(t('Please_enter_value_for_url')); } + } else if (this.service === 'upload') { + let files = instance.find('input[type=file]').files; + if (!files || files.length === 0) { + files = event.dataTransfer && event.dataTransfer.files || []; + } + + for (const file of files) { + Object.defineProperty(file, 'type', { value: mime.lookup(file.name) }); + } + + const record = { + name: files[0].name, + size: files[0].size, + type: files[0].type + // description: document.getElementById('file-description').value + }; + + const upload = fileUploadHandler('Avatars', record, files[0]); + + // upload.onProgress = (progress) -> + // console.log 'progress ->', progress + + upload.start((error, result) => { + if (result) { + toastr.success(t('Avatar_changed_successfully')); + RocketChat.callbacks.run('userAvatarSet', this.service); + } + }); } else { const tmpService = this.service; Meteor.call('setAvatarFromService', this.blob, this.contentType, this.service, function(err) { - if (err && err.details.timeToReset && err.details.timeToReset) { + if (err && err.details && err.details.timeToReset) { toastr.error(t('error-too-many-requests', { seconds: parseInt(err.details.timeToReset / 1000) })); diff --git a/packages/rocketchat-ui-master/.npm/package/npm-shrinkwrap.json b/packages/rocketchat-ui-master/.npm/package/npm-shrinkwrap.json index 7db964c3bb1a..6a2270165462 100644 --- a/packages/rocketchat-ui-master/.npm/package/npm-shrinkwrap.json +++ b/packages/rocketchat-ui-master/.npm/package/npm-shrinkwrap.json @@ -1,28 +1,28 @@ { "dependencies": { "clipboard": { - "version": "1.5.12", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-1.5.12.tgz", - "from": "clipboard@1.5.12" + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-1.6.1.tgz", + "from": "clipboard@1.6.1" }, "delegate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.1.0.tgz", - "from": "delegate@>=3.1.0 <4.0.0" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.1.2.tgz", + "from": "delegate@>=3.1.2 <4.0.0" }, "good-listener": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.0.tgz", - "from": "good-listener@>=1.1.6 <2.0.0" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "from": "good-listener@>=1.2.0 <2.0.0" }, "select": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/select/-/select-1.1.0.tgz", - "from": "select@>=1.0.6 <2.0.0" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "from": "select@>=1.1.2 <2.0.0" }, "tiny-emitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-1.1.0.tgz", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-1.2.0.tgz", "from": "tiny-emitter@>=1.0.0 <2.0.0" } } diff --git a/packages/rocketchat-ui/client/lib/fileUpload.coffee b/packages/rocketchat-ui/client/lib/fileUpload.coffee deleted file mode 100644 index e4494b7d7868..000000000000 --- a/packages/rocketchat-ui/client/lib/fileUpload.coffee +++ /dev/null @@ -1,189 +0,0 @@ -readAsDataURL = (file, callback) -> - reader = new FileReader() - reader.onload = (ev) -> - callback ev.target.result, file - - reader.readAsDataURL file - -getUploadPreview = (file, callback) -> - # If greater then 10MB don't try and show a preview - if file.file.size > 10 * 1000000 - callback(file, null) - else if not file.file.type? - callback(file, null) - else - if file.file.type.indexOf('audio') > -1 or file.file.type.indexOf('video') > -1 or file.file.type.indexOf('image') > -1 - file.type = file.file.type.split('/')[0] - - readAsDataURL file.file, (content) -> - callback(file, content) - else - callback(file, null) - -formatBytes = (bytes, decimals) -> - if bytes == 0 - return '0 Bytes' - - k = 1000 - dm = decimals + 1 or 3 - - sizes = [ - 'Bytes' - 'KB' - 'MB' - 'GB' - 'TB' - 'PB' - ] - - i = Math.floor(Math.log(bytes) / Math.log(k)) - - parseFloat((bytes / k ** i).toFixed(dm)) + ' ' + sizes[i] - -readAsArrayBuffer = (file, callback) -> - reader = new FileReader() - reader.onload = (ev) -> - callback ev.target.result, file - - reader.readAsArrayBuffer file - - -@fileUpload = (files) -> - roomId = Session.get('openedRoom') - files = [].concat files - - consume = -> - file = files.pop() - if not file? - swal.close() - return - - if not RocketChat.fileUploadIsValidContentType file.file.type - swal - title: t('FileUpload_MediaType_NotAccepted') - text: file.file.type || "*.#{s.strRightBack(file.file.name, '.')}" - type: 'error' - timer: 3000 - return - - if file.file.size is 0 - swal - title: t('FileUpload_File_Empty') - type: 'error' - timer: 1000 - return - - getUploadPreview file, (file, preview) -> - text = '' - - if file.type is 'audio' - text = """ -
- -
-
- - -
- """ - else if file.type is 'video' - text = """ -
- -
-
- - -
- """ - else if file.type is 'image' - text = """ -
-
-
-
- - -
- """ - else - fileSize = formatBytes(file.file.size) - - text = """ -
-
#{Handlebars._escape(file.name)} - #{fileSize}
-
-
- - -
- """ - - swal - title: t('Upload_file_question') - text: text - showCancelButton: true - closeOnConfirm: false - closeOnCancel: false - confirmButtonText: t('Send') - cancelButtonText: t('Cancel') - html: true - , (isConfirm) -> - consume() - if isConfirm isnt true - return - - record = - name: document.getElementById('file-name').value or file.name or file.file.name - size: file.file.size - type: file.file.type - rid: roomId - description: document.getElementById('file-description').value - - upload = fileUploadHandler record, file.file - - uploading = Session.get('uploading') or [] - uploading.push - id: upload.id - name: upload.getFileName() - percentage: 0 - - Session.set 'uploading', uploading - - upload.onProgress = (progress) -> - uploading = Session.get('uploading') - - item = _.findWhere(uploading, {id: upload.id}) - if item? - item.percentage = Math.round(progress * 100) or 0 - Session.set 'uploading', uploading - - upload.start() - - Tracker.autorun (c) -> - cancel = Session.get "uploading-cancel-#{upload.id}" - if cancel - upload.stop() - c.stop() - - uploading = Session.get 'uploading' - if uploading? - item = _.findWhere(uploading, {id: upload.id}) - if item? - item.percentage = 0 - Session.set 'uploading', uploading - - Meteor.setTimeout -> - uploading = Session.get 'uploading' - if uploading? - item = _.findWhere(uploading, {id: upload.id}) - Session.set 'uploading', _.without(uploading, item) - , 1000 - - consume() diff --git a/packages/rocketchat-ui/client/lib/fileUpload.js b/packages/rocketchat-ui/client/lib/fileUpload.js new file mode 100644 index 000000000000..9c20385e3021 --- /dev/null +++ b/packages/rocketchat-ui/client/lib/fileUpload.js @@ -0,0 +1,241 @@ +/* globals fileUploadHandler, Handlebars, fileUpload */ +/* exported fileUpload */ + +function readAsDataURL(file, callback) { + const reader = new FileReader(); + reader.onload = ev => callback(ev.target.result, file); + + return reader.readAsDataURL(file); +} + +function getUploadPreview(file, callback) { + // If greater then 10MB don't try and show a preview + if (file.file.size > (10 * 1000000)) { + return callback(file, null); + } else if (file.file.type == null) { + callback(file, null); + } else if ((file.file.type.indexOf('audio') > -1) || (file.file.type.indexOf('video') > -1) || (file.file.type.indexOf('image') > -1)) { + file.type = file.file.type.split('/')[0]; + + return readAsDataURL(file.file, content => callback(file, content)); + } else { + return callback(file, null); + } +} + +function formatBytes(bytes, decimals) { + if (bytes === 0) { + return '0 Bytes'; + } + + const k = 1000; + const dm = (decimals + 1) || 3; + + const sizes = [ + 'Bytes', + 'KB', + 'MB', + 'GB', + 'TB', + 'PB' + ]; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${ parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) } ${ sizes[i] }`; +} + +fileUpload = function(filesToUpload) { + const roomId = Session.get('openedRoom'); + const files = [].concat(filesToUpload); + + function consume() { + const file = files.pop(); + if ((file == null)) { + swal.close(); + return; + } + + if (!RocketChat.fileUploadIsValidContentType(file.file.type)) { + swal({ + title: t('FileUpload_MediaType_NotAccepted'), + text: file.file.type || `*.${ s.strRightBack(file.file.name, '.') }`, + type: 'error', + timer: 3000 + }); + return; + } + + if (file.file.size === 0) { + swal({ + title: t('FileUpload_File_Empty'), + type: 'error', + timer: 1000 + }); + return; + } + + return getUploadPreview(file, function(file, preview) { + let text = ''; + + if (file.type === 'audio') { + text = `\ +
+ +
+
+ + +
`; + } else if (file.type === 'video') { + text = `\ +
+ +
+
+ + +
`; + } else if (file.type === 'image') { + text = `\ +
+
+
+
+ + +
`; + } else { + const fileSize = formatBytes(file.file.size); + + text = `\ +
+
${ Handlebars._escape(file.name) } - ${ fileSize }
+
+
+ + +
`; + } + + return swal({ + title: t('Upload_file_question'), + text, + showCancelButton: true, + closeOnConfirm: false, + closeOnCancel: false, + confirmButtonText: t('Send'), + cancelButtonText: t('Cancel'), + html: true + }, function(isConfirm) { + consume(); + if (isConfirm !== true) { + return; + } + + const record = { + name: document.getElementById('file-name').value || file.name || file.file.name, + size: file.file.size, + type: file.file.type, + rid: roomId, + description: document.getElementById('file-description').value + }; + + const upload = fileUploadHandler('Uploads', record, file.file); + + let uploading = Session.get('uploading') || []; + uploading.push({ + id: upload.id, + name: upload.getFileName(), + percentage: 0 + }); + + Session.set('uploading', uploading); + + upload.onProgress = function(progress) { + uploading = Session.get('uploading'); + + const item = _.findWhere(uploading, {id: upload.id}); + if (item != null) { + item.percentage = Math.round(progress * 100) || 0; + return Session.set('uploading', uploading); + } + }; + + upload.start(function(error, file, storage) { + if (error) { + let uploading = Session.get('uploading'); + if (!Array.isArray(uploading)) { + uploading = []; + } + + const item = _.findWhere(uploading, { id: this.id }); + + if (_.isObject(item)) { + item.error = error.error; + item.percentage = 0; + } else { + uploading.push({ + error: error.error, + percentage: 0 + }); + } + + Session.set('uploading', uploading); + return; + } + + + if (file) { + Meteor.call('sendFileMessage', roomId, storage, file, () => { + Meteor.setTimeout(() => { + const uploading = Session.get('uploading'); + if (uploading !== null) { + const item = _.findWhere(uploading, { + id: upload.id + }); + return Session.set('uploading', _.without(uploading, item)); + } + }, 2000); + }); + } + }); + + Tracker.autorun(function(c) { + const cancel = Session.get(`uploading-cancel-${ upload.id }`); + if (cancel) { + let item; + upload.stop(); + c.stop(); + + uploading = Session.get('uploading'); + if (uploading != null) { + item = _.findWhere(uploading, {id: upload.id}); + if (item != null) { + item.percentage = 0; + } + Session.set('uploading', uploading); + } + + return Meteor.setTimeout(function() { + uploading = Session.get('uploading'); + if (uploading != null) { + item = _.findWhere(uploading, {id: upload.id}); + return Session.set('uploading', _.without(uploading, item)); + } + } + , 1000); + } + }); + }); + }); + } + + consume(); +}; diff --git a/packages/rocketchat-ui/package.js b/packages/rocketchat-ui/package.js index 44831edb8326..bf9e8e211473 100644 --- a/packages/rocketchat-ui/package.js +++ b/packages/rocketchat-ui/package.js @@ -38,7 +38,7 @@ Package.onUse(function(api) { api.addFiles('client/lib/chatMessages.js', 'client'); api.addFiles('client/lib/collections.js', 'client'); api.addFiles('client/lib/customEventPolyfill.js', 'client'); - api.addFiles('client/lib/fileUpload.coffee', 'client'); + api.addFiles('client/lib/fileUpload.js', 'client'); api.addFiles('client/lib/fireEvent.js', 'client'); api.addFiles('client/lib/iframeCommands.js', 'client'); api.addFiles('client/lib/menu.js', 'client'); @@ -108,4 +108,6 @@ Package.onUse(function(api) { api.addFiles('client/views/app/videoCall/videoButtons.js', 'client'); api.addFiles('client/views/app/videoCall/videoCall.js', 'client'); api.addFiles('client/views/app/photoswipe.js', 'client'); + + api.export('fileUpload'); }); diff --git a/server/methods/deleteFileMessage.js b/server/methods/deleteFileMessage.js index ce10e596c459..9ffc1fe7f8a6 100644 --- a/server/methods/deleteFileMessage.js +++ b/server/methods/deleteFileMessage.js @@ -9,6 +9,6 @@ Meteor.methods({ return Meteor.call('deleteMessage', msg); } - return FileUpload.delete(fileID); + return FileUpload.getStore('Uploads').deleteById(fileID); } }); diff --git a/server/methods/resetAvatar.js b/server/methods/resetAvatar.js index 8b5d1fed2191..ea551e50850f 100644 --- a/server/methods/resetAvatar.js +++ b/server/methods/resetAvatar.js @@ -13,7 +13,7 @@ Meteor.methods({ } const user = Meteor.user(); - RocketChatFileAvatarInstance.deleteFile(`${ user.username }.jpg`); + FileUpload.getStore('Avatars').deleteByName(user.username); RocketChat.models.Users.unsetAvatarOrigin(user._id); RocketChat.Notifications.notifyLogged('updateAvatar', { username: user.username diff --git a/server/startup/avatar.js b/server/startup/avatar.js index 6aca22c66592..0fa118f685e1 100644 --- a/server/startup/avatar.js +++ b/server/startup/avatar.js @@ -1,47 +1,10 @@ -import getFileType from 'file-type'; - +/* globals FileUpload */ Meteor.startup(function() { - let storeType = 'GridFS'; - - if (RocketChat.settings.get('Accounts_AvatarStoreType')) { - storeType = RocketChat.settings.get('Accounts_AvatarStoreType'); - } - - const RocketChatStore = RocketChatFile[storeType]; - - if (!RocketChatStore) { - throw new Error(`Invalid RocketChatStore type [${ storeType }]`); - } - - console.log((`Using ${ storeType } for Avatar storage`).green); - - function transformWrite(file, readStream, writeStream) { - if (RocketChatFile.enabled === false || RocketChat.settings.get('Accounts_AvatarResize') !== true) { - return readStream.pipe(writeStream); - } - const height = RocketChat.settings.get('Accounts_AvatarSize'); - const width = height; - return RocketChatFile.gm(readStream, file.fileName).background('#ffffff').resize(width, `${ height }^`).gravity('Center').crop(width, height).extent(width, height).stream('jpeg').pipe(writeStream); - } - - let path = '~/uploads'; - if (RocketChat.settings.get('Accounts_AvatarStorePath') && RocketChat.settings.get('Accounts_AvatarStorePath').trim() !== '') { - path = RocketChat.settings.get('Accounts_AvatarStorePath'); - } - - this.RocketChatFileAvatarInstance = new RocketChatStore({ - name: 'avatars', - absolutePath: path, - transformWrite - }); - - return WebApp.connectHandlers.use('/avatar/', Meteor.bindEnvironment(function(req, res/*, next*/) { + WebApp.connectHandlers.use('/avatar/', Meteor.bindEnvironment(function(req, res/*, next*/) { const params = { username: decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, '')) }; - let username = params.username.replace(/\.jpg$/, '').replace(/^@/, ''); - if (_.isEmpty(params.username)) { res.writeHead(403); res.write('Forbidden'); @@ -49,106 +12,87 @@ Meteor.startup(function() { return; } - let file; + const match = /^\/([^?]*)/.exec(req.url); - if (params.username[0] !== '@') { - if (Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm) { - const user = RocketChat.models.Users.findOneByUsername(username); - if (user && user.services && user.services.sandstorm && user.services.sandstorm.picture) { - res.setHeader('Location', user.services.sandstorm.picture); - res.writeHead(302); - res.end(); - return; - } - } - - file = RocketChatFileAvatarInstance.getFileWithReadStream(encodeURIComponent(`${ username }.jpg`)); - } - - res.setHeader('Content-Disposition', 'inline'); - if (!file) { - res.setHeader('Content-Type', 'image/svg+xml'); - res.setHeader('Cache-Control', 'public, max-age=0'); - res.setHeader('Expires', '-1'); - res.setHeader('Last-Modified', 'Thu, 01 Jan 2015 00:00:00 GMT'); + if (match[1]) { + let username = decodeURIComponent(match[1]); + let file; - const reqModifiedHeader = req.headers['if-modified-since']; - - if (reqModifiedHeader) { - if (reqModifiedHeader === 'Thu, 01 Jan 2015 00:00:00 GMT') { - res.writeHead(304); - res.end(); - return; + if (username[0] !== '@') { + if (Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm) { + const user = RocketChat.models.Users.findOneByUsername(username); + if (user && user.services && user.services.sandstorm && user.services.sandstorm.picture) { + res.setHeader('Location', user.services.sandstorm.picture); + res.writeHead(302); + res.end(); + return; + } } + file = RocketChat.models.Avatars.findOneByName(username); } - const colors = ['#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFC107', '#FF9800', '#FF5722', '#795548', '#9E9E9E', '#607D8B']; + if (file) { + res.setHeader('Content-Security-Policy', 'default-src \'none\''); - if (RocketChat.settings.get('UI_Use_Name_Avatar')) { - const user = RocketChat.models.Users.findOneByUsername(username, { - fields: { - name: 1 + return FileUpload.get(file, req, res); + } else { + res.setHeader('Content-Type', 'image/svg+xml'); + res.setHeader('Cache-Control', 'public, max-age=0'); + res.setHeader('Expires', '-1'); + res.setHeader('Last-Modified', 'Thu, 01 Jan 2015 00:00:00 GMT'); + + const reqModifiedHeader = req.headers['if-modified-since']; + + if (reqModifiedHeader) { + if (reqModifiedHeader === 'Thu, 01 Jan 2015 00:00:00 GMT') { + res.writeHead(304); + res.end(); + return; } - }); - - if (user && user.name) { - username = user.name; } - } - let color = ''; - let initials = ''; + const colors = ['#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFC107', '#FF9800', '#FF5722', '#795548', '#9E9E9E', '#607D8B']; - if (username === '?') { - color = '#000'; - initials = username; - } else { - const position = username.length % colors.length; + if (RocketChat.settings.get('UI_Use_Name_Avatar')) { + const user = RocketChat.models.Users.findOneByUsername(username, { + fields: { + name: 1 + } + }); - color = colors[position]; - username = username.replace(/[^A-Za-z0-9]/g, '.').replace(/\.+/g, '.').replace(/(^\.)|(\.$)/g, ''); + if (user && user.name) { + username = user.name; + } + } - const usernameParts = username.split('.'); + let color = ''; + let initials = ''; - initials = usernameParts.length > 1 ? _.first(usernameParts)[0] + _.last(usernameParts)[0] : username.replace(/[^A-Za-z0-9]/g, '').substr(0, 2); - initials = initials.toUpperCase(); - } + if (username === '?') { + color = '#000'; + initials = username; + } else { + const position = username.length % colors.length; - const svg = `\n\n \n ${ initials }\n \n`; - res.write(svg); - res.end(); + color = colors[position]; + username = username.replace(/[^A-Za-z0-9]/g, '.').replace(/\.+/g, '.').replace(/(^\.)|(\.$)/g, ''); - return; - } + const usernameParts = username.split('.'); + + initials = usernameParts.length > 1 ? _.first(usernameParts)[0] + _.last(usernameParts)[0] : username.replace(/[^A-Za-z0-9]/g, '').substr(0, 2); + initials = initials.toUpperCase(); + } - const reqModifiedHeader = req.headers['if-modified-since']; - if (reqModifiedHeader) { - if (reqModifiedHeader === (file.uploadDate && file.uploadDate.toUTCString())) { - res.setHeader('Last-Modified', reqModifiedHeader); - res.writeHead(304); + const svg = `\n\n \n ${ initials }\n \n`; + res.write(svg); res.end(); + return; } } - res.setHeader('Cache-Control', 'public, max-age=0'); - res.setHeader('Expires', '-1'); - res.setHeader('Last-Modified', (file.uploadDate && file.uploadDate.toUTCString()) || new Date().toUTCString()); - res.setHeader('Content-Length', file.length); - - if (file.contentType) { - res.setHeader('Content-Type', file.contentType); - } else { - file.readStream.once('data', function(chunk) { - const fileType = getFileType(chunk); - if (fileType) { - return res.setHeader('Content-Type', fileType.mime); - } else { - return res.setHeader('Content-Type', 'image/jpeg'); - } - }); - } - - file.readStream.pipe(res); + res.writeHead(404); + res.end(); + return; })); }); diff --git a/server/startup/initialData.js b/server/startup/initialData.js index 298d76c19119..b8f9acdcaf37 100644 --- a/server/startup/initialData.js +++ b/server/startup/initialData.js @@ -21,16 +21,19 @@ Meteor.startup(function() { RocketChat.authz.addUserRoles('rocket.cat', 'bot'); const rs = RocketChatFile.bufferToStream(new Buffer(Assets.getBinary('avatars/rocketcat.png'), 'utf8')); + const fileStore = FileUpload.getStore('Avatars'); + fileStore.deleteByName('rocket.cat'); - RocketChatFileAvatarInstance.deleteFile('rocket.cat.jpg'); - - const ws = RocketChatFileAvatarInstance.createWriteStream('rocket.cat.jpg', 'image/png'); - - ws.on('end', Meteor.bindEnvironment(function() { - return RocketChat.models.Users.setAvatarOrigin('rocket.cat', 'local'); - })); + const file = { + userId: 'rocket.cat', + type: 'image/png' + }; - rs.pipe(ws); + Meteor.runAsUser('rocket.cat', () => { + fileStore.insert(file, rs, () => { + return RocketChat.models.Users.setAvatarOrigin('rocket.cat', 'local'); + }); + }); } if (process.env.ADMIN_PASS) { diff --git a/server/startup/migrations/v002.js b/server/startup/migrations/v002.js index ad2d589a3cb8..e67336ed640d 100644 --- a/server/startup/migrations/v002.js +++ b/server/startup/migrations/v002.js @@ -26,13 +26,17 @@ RocketChat.Migrations.add({ const {image, contentType} = RocketChatFile.dataURIParse(dataURI); const rs = RocketChatFile.bufferToStream(new Buffer(image, 'base64')); - const ws = RocketChatFileAvatarInstance.createWriteStream(`${ user.username }.jpg`, contentType); + const fileStore = FileUpload.getStore('Avatars'); + fileStore.deleteByName(user.username); - ws.on('end', Meteor.bindEnvironment(function() { - return RocketChat.models.Users.setAvatarOrigin(user._id, service); - })); + const file = { + userId: user._id, + type: contentType + }; - return rs.pipe(ws); + fileStore.insert(file, rs, () => { + return RocketChat.models.Users.setAvatarOrigin(user._id, service); + }); }); } }); diff --git a/server/startup/migrations/v097.js b/server/startup/migrations/v097.js new file mode 100644 index 000000000000..a6554a37575f --- /dev/null +++ b/server/startup/migrations/v097.js @@ -0,0 +1,144 @@ +import fs from 'fs'; +import path from 'path'; + +RocketChat.Migrations.add({ + version: 96, + up() { + const query = { + $or: [{ + 's3.path': { + $exists: true + } + }, { + 'googleCloudStorage.path': { + $exists: true + } + }] + }; + + RocketChat.models.Uploads.find(query).forEach((record) => { + if (record.s3) { + RocketChat.models.Uploads.model.direct.update({_id: record._id}, { + $set: { + 'store': 'AmazonS3:Uploads', + AmazonS3: { + path: record.s3.path + record._id + } + }, + $unset: { + s3: 1 + } + }, {multi: true}); + } else { + RocketChat.models.Uploads.model.direct.update({_id: record._id}, { + $set: { + store: 'GoogleCloudStorage:Uploads', + GoogleStorage: { + path: record.googleCloudStorage.path + record._id + } + }, + $unset: { + googleCloudStorage: 1 + } + }, {multi: true}); + } + }); + + RocketChat.models.Uploads.model.direct.update({ + store: 'fileSystem' + }, { + $set: { + store: 'FileSystem:Uploads' + } + }, { + multi: true + }); + RocketChat.models.Uploads.model.direct.update({ + store: 'rocketchat_uploads' + }, { + $set: { + store: 'GridFS:Uploads' + } + }, { + multi: true + }); + + const avatarOrigins = [ + 'upload', + 'gravatar', + 'facebook', + 'twitter', + 'github', + 'google', + 'url', + 'gitlab', + 'linkedin' + ]; + + Meteor.setTimeout(function() { + Meteor.runAsUser('rocket.cat', function() { + const avatarsFileStore = FileUpload.getStore('Avatars'); + + const oldAvatarGridFS = new RocketChatFile.GridFS({ + name: 'avatars' + }); + + RocketChat.models.Users.find({avatarOrigin: {$in: avatarOrigins}}, {avatarOrigin: 1, username: 1}).forEach((user) => { + const id = `${ user.username }.jpg`; + const gridFSAvatar = oldAvatarGridFS.getFileWithReadStream(id); + + if (gridFSAvatar) { + const details = { + userId: user._id, + type: gridFSAvatar.contentType, + size: gridFSAvatar.length, + name: user.username + }; + + avatarsFileStore.insert(details, gridFSAvatar.readStream, (err) => { + if (err) { + console.log({err}); + } else { + oldAvatarGridFS.deleteFile(id); + } + }); + } else { + const avatarsPath = RocketChat.models.Settings.findOne({_id: 'Accounts_AvatarStorePath'}).value; + if (avatarsPath && avatarsPath.trim()) { + const filePath = path.join(avatarsPath, id); + try { + const stat = fs.statSync(filePath); + if (stat && stat.isFile()) { + const rs = fs.createReadStream(filePath); + const details = { + userId: user._id, + type: 'image/jpeg', + size: stat.size + }; + + avatarsFileStore.insert(details, rs, (err) => { + if (err) { + console.log({err}); + } else { + fs.unlinkSync(filePath); + } + }); + } + } catch (e) { + console.log('Error migrating old avatars', e); + } + } + } + }); + + RocketChat.models.Settings.remove({_id: 'Accounts_AvatarStoreType'}); + RocketChat.models.Settings.remove({_id: 'Accounts_AvatarStorePath'}); + + const avatarsFiles = new Mongo.Collection('avatars.files'); + const avatarsChunks = new Mongo.Collection('avatars.chunks'); + avatarsFiles.rawCollection().drop(); + avatarsChunks.rawCollection().drop(); + }); + }, 1000); + } +});