diff --git a/server/api/controllers/attachments/create.js b/server/api/controllers/attachments/create.js index 38428ce9..e822e5e5 100644 --- a/server/api/controllers/attachments/create.js +++ b/server/api/controllers/attachments/create.js @@ -1,3 +1,6 @@ +const util = require('util'); +const { v4: uuid } = require('uuid'); + const Errors = { NOT_ENOUGH_RIGHTS: { notEnoughRights: 'Not enough rights', @@ -5,6 +8,9 @@ const Errors = { CARD_NOT_FOUND: { cardNotFound: 'Card not found', }, + NO_FILE_WAS_UPLOADED: { + noFileWasUploaded: 'No file was uploaded', + }, }; module.exports = { @@ -27,6 +33,9 @@ module.exports = { cardNotFound: { responseType: 'notFound', }, + noFileWasUploaded: { + responseType: 'unprocessableEntity', + }, uploadError: { responseType: 'unprocessableEntity', }, @@ -52,33 +61,37 @@ module.exports = { throw Errors.NOT_ENOUGH_RIGHTS; } - this.req - .file('file') - .upload(sails.helpers.utils.createAttachmentReceiver(), async (error, files) => { - if (error) { - return exits.uploadError(error.message); - } + const upload = util.promisify((options, callback) => + this.req.file('file').upload(options, (error, files) => callback(error, files)), + ); + + let files; + try { + files = await upload({ + saveAs: uuid(), + maxBytes: null, + }); + } catch (error) { + return exits.uploadError(error.message); // TODO: add error + } - if (files.length === 0) { - return exits.uploadError('No file was uploaded'); - } + if (files.length === 0) { + throw Errors.NO_FILE_WAS_UPLOADED; + } - const file = files[0]; + const file = _.last(files); + const fileData = await sails.helpers.attachments.processUploadedFile(file); - const attachment = await sails.helpers.attachments.createOne( - { - ...file.extra, - filename: file.filename, - }, - currentUser, - card, - inputs.requestId, - this.req, - ); + const attachment = await sails.helpers.attachments.createOne( + fileData, + currentUser, + card, + inputs.requestId, + this.req, + ); - return exits.success({ - item: attachment.toJSON(), - }); - }); + return exits.success({ + item: attachment, + }); }, }; diff --git a/server/api/controllers/cards/index.js b/server/api/controllers/cards/index.js index 1515a7a0..3bcd7ea0 100644 --- a/server/api/controllers/cards/index.js +++ b/server/api/controllers/cards/index.js @@ -23,7 +23,7 @@ module.exports = { }, }, - async fn(inputs, exits) { + async fn(inputs) { const { currentUser } = this.req; const { board } = await sails.helpers.boards @@ -61,7 +61,7 @@ module.exports = { const tasks = await sails.helpers.cards.getTasks(cardIds); const attachments = await sails.helpers.cards.getAttachments(cardIds); - return exits.success({ + return { items: cards, included: { cardMemberships, @@ -69,6 +69,6 @@ module.exports = { tasks, attachments, }, - }); + }; }, }; diff --git a/server/api/controllers/projects/update-background-image.js b/server/api/controllers/projects/update-background-image.js index 372b5c06..082e8d22 100755 --- a/server/api/controllers/projects/update-background-image.js +++ b/server/api/controllers/projects/update-background-image.js @@ -1,7 +1,17 @@ +const util = require('util'); +const rimraf = require('rimraf'); +const { v4: uuid } = require('uuid'); + const Errors = { PROJECT_NOT_FOUND: { projectNotFound: 'Project not found', }, + NO_FILE_WAS_UPLOADED: { + noFileWasUploaded: 'No file was uploaded', + }, + FILE_IS_NOT_IMAGE: { + fileIsNotImage: 'File is not image', + }, }; module.exports = { @@ -17,6 +27,12 @@ module.exports = { projectNotFound: { responseType: 'notFound', }, + noFileWasUploaded: { + responseType: 'unprocessableEntity', + }, + fileIsNotImage: { + responseType: 'unprocessableEntity', + }, uploadError: { responseType: 'unprocessableEntity', }, @@ -37,32 +53,52 @@ module.exports = { throw Errors.PROJECT_NOT_FOUND; // Forbidden } - this.req - .file('file') - .upload(sails.helpers.utils.createProjectBackgroundImageReceiver(), async (error, files) => { - if (error) { - return exits.uploadError(error.message); - } + const upload = util.promisify((options, callback) => + this.req.file('file').upload(options, (error, files) => callback(error, files)), + ); - if (files.length === 0) { - return exits.uploadError('No file was uploaded'); - } + let files; + try { + files = await upload({ + saveAs: uuid(), + maxBytes: null, + }); + } catch (error) { + return exits.uploadError(error.message); // TODO: add error + } + + if (files.length === 0) { + throw Errors.NO_FILE_WAS_UPLOADED; + } - project = await sails.helpers.projects.updateOne( - project, - { - backgroundImageDirname: files[0].extra.dirname, - }, - this.req, - ); + const file = _.last(files); - if (!project) { - throw Errors.PROJECT_NOT_FOUND; + const fileData = await sails.helpers.projects + .processUploadedBackgroundImageFile(file) + .intercept('fileIsNotImage', () => { + try { + rimraf.sync(file.fd); + } catch (error) { + console.warn(error.stack); // eslint-disable-line no-console } - return exits.success({ - item: project.toJSON(), - }); + return Errors.FILE_IS_NOT_IMAGE; }); + + project = await sails.helpers.projects.updateOne( + project, + { + backgroundImageDirname: fileData.dirname, + }, + this.req, + ); + + if (!project) { + throw Errors.PROJECT_NOT_FOUND; + } + + return exits.success({ + item: project, + }); }, }; diff --git a/server/api/controllers/users/update-avatar.js b/server/api/controllers/users/update-avatar.js index 66635b7f..1e74ffe6 100755 --- a/server/api/controllers/users/update-avatar.js +++ b/server/api/controllers/users/update-avatar.js @@ -1,7 +1,17 @@ +const util = require('util'); +const rimraf = require('rimraf'); +const { v4: uuid } = require('uuid'); + const Errors = { USER_NOT_FOUND: { userNotFound: 'User not found', }, + NO_FILE_WAS_UPLOADED: { + noFileWasUploaded: 'No file was uploaded', + }, + FILE_IS_NOT_IMAGE: { + fileIsNotImage: 'File is not image', + }, }; module.exports = { @@ -17,6 +27,12 @@ module.exports = { userNotFound: { responseType: 'notFound', }, + noFileWasUploaded: { + responseType: 'unprocessableEntity', + }, + fileIsNotImage: { + responseType: 'unprocessableEntity', + }, uploadError: { responseType: 'unprocessableEntity', }, @@ -38,33 +54,53 @@ module.exports = { user = currentUser; } - this.req - .file('file') - .upload(sails.helpers.utils.createUserAvatarReceiver(), async (error, files) => { - if (error) { - return exits.uploadError(error.message); - } + const upload = util.promisify((options, callback) => + this.req.file('file').upload(options, (error, files) => callback(error, files)), + ); - if (files.length === 0) { - return exits.uploadError('No file was uploaded'); - } + let files; + try { + files = await upload({ + saveAs: uuid(), + maxBytes: null, + }); + } catch (error) { + return exits.uploadError(error.message); // TODO: add error + } + + if (files.length === 0) { + throw Errors.NO_FILE_WAS_UPLOADED; + } - user = await sails.helpers.users.updateOne( - user, - { - avatarDirname: files[0].extra.dirname, - }, - currentUser, - this.req, - ); + const file = _.last(files); - if (!user) { - throw Errors.USER_NOT_FOUND; + const fileData = await sails.helpers.users + .processUploadedAvatarFile(file) + .intercept('fileIsNotImage', () => { + try { + rimraf.sync(file.fd); + } catch (error) { + console.warn(error.stack); // eslint-disable-line no-console } - return exits.success({ - item: user.toJSON(), - }); + return Errors.FILE_IS_NOT_IMAGE; }); + + user = await sails.helpers.users.updateOne( + user, + { + avatarDirname: fileData.dirname, + }, + currentUser, + this.req, + ); + + if (!user) { + throw Errors.USER_NOT_FOUND; + } + + return exits.success({ + item: user, + }); }, }; diff --git a/server/api/helpers/attachments/process-uploaded-file.js b/server/api/helpers/attachments/process-uploaded-file.js new file mode 100644 index 00000000..21b7b270 --- /dev/null +++ b/server/api/helpers/attachments/process-uploaded-file.js @@ -0,0 +1,79 @@ +const fs = require('fs'); +const path = require('path'); +const util = require('util'); +const rimraf = require('rimraf'); +const filenamify = require('filenamify'); +const { v4: uuid } = require('uuid'); +const sharp = require('sharp'); + +const rename = util.promisify(fs.rename); + +module.exports = { + inputs: { + file: { + type: 'json', + required: true, + }, + }, + + async fn(inputs) { + const dirname = uuid(); + + // FIXME: https://github.com/sindresorhus/filenamify/issues/13 + const filename = filenamify(inputs.file.filename); + + const rootPath = path.join(sails.config.custom.attachmentsPath, dirname); + const filePath = path.join(rootPath, filename); + + fs.mkdirSync(rootPath); + await rename(inputs.file.fd, filePath); + + const image = sharp(filePath); + let metadata; + + try { + metadata = await image.metadata(); + } catch (error) {} // eslint-disable-line no-empty + + const fileData = { + dirname, + filename, + image: null, + name: inputs.file.filename, + }; + + if (metadata && !['svg', 'pdf'].includes(metadata.format)) { + const thumbnailsPath = path.join(rootPath, 'thumbnails'); + fs.mkdirSync(thumbnailsPath); + + try { + await image + .resize( + metadata.height > metadata.width + ? { + width: 256, + height: 320, + } + : { + width: 256, + }, + ) + .jpeg({ + quality: 100, + chromaSubsampling: '4:4:4', + }) + .toFile(path.join(thumbnailsPath, 'cover-256.jpg')); + + fileData.image = _.pick(metadata, ['width', 'height']); + } catch (error1) { + try { + rimraf.sync(thumbnailsPath); + } catch (error2) { + console.warn(error2.stack); // eslint-disable-line no-console + } + } + } + + return fileData; + }, +}; diff --git a/server/api/helpers/projects/process-uploaded-background-image-file.js b/server/api/helpers/projects/process-uploaded-background-image-file.js new file mode 100644 index 00000000..93f62f34 --- /dev/null +++ b/server/api/helpers/projects/process-uploaded-background-image-file.js @@ -0,0 +1,63 @@ +const fs = require('fs'); +const path = require('path'); +const rimraf = require('rimraf'); +const { v4: uuid } = require('uuid'); +const sharp = require('sharp'); + +module.exports = { + inputs: { + file: { + type: 'json', + required: true, + }, + }, + + exits: { + fileIsNotImage: {}, + }, + + async fn(inputs) { + const image = sharp(inputs.file.fd); + + try { + await image.metadata(); + } catch (error) { + throw 'fileIsNotImage'; + } + + const dirname = uuid(); + const rootPath = path.join(sails.config.custom.projectBackgroundImagesPath, dirname); + + fs.mkdirSync(rootPath); + + try { + await image.jpeg().toFile(path.join(rootPath, 'original.jpg')); + + await image + .resize(336, 200) + .jpeg({ + quality: 100, + chromaSubsampling: '4:4:4', + }) + .toFile(path.join(rootPath, 'cover-336.jpg')); + } catch (error1) { + try { + rimraf.sync(rootPath); + } catch (error2) { + console.warn(error2.stack); // eslint-disable-line no-console + } + + throw 'fileIsNotImage'; + } + + try { + rimraf.sync(inputs.file.fd); + } catch (error) { + console.warn(error.stack); // eslint-disable-line no-console + } + + return { + dirname, + }; + }, +}; diff --git a/server/api/helpers/users/process-uploaded-avatar-file.js b/server/api/helpers/users/process-uploaded-avatar-file.js new file mode 100644 index 00000000..5658b7be --- /dev/null +++ b/server/api/helpers/users/process-uploaded-avatar-file.js @@ -0,0 +1,68 @@ +const fs = require('fs'); +const path = require('path'); +const rimraf = require('rimraf'); +const { v4: uuid } = require('uuid'); +const sharp = require('sharp'); + +module.exports = { + inputs: { + file: { + type: 'json', + required: true, + }, + }, + + exits: { + fileIsNotImage: {}, + }, + + async fn(inputs) { + const image = sharp(inputs.file.fd); + + try { + await image.metadata(); + } catch (error) { + throw 'fileIsNotImage'; + } + + const dirname = uuid(); + const rootPath = path.join(sails.config.custom.userAvatarsPath, dirname); + + fs.mkdirSync(rootPath); + + try { + await image + .jpeg({ + quality: 100, + chromaSubsampling: '4:4:4', + }) + .toFile(path.join(rootPath, 'original.jpg')); + + await image + .resize(100, 100) + .jpeg({ + quality: 100, + chromaSubsampling: '4:4:4', + }) + .toFile(path.join(rootPath, 'square-100.jpg')); + } catch (error1) { + try { + rimraf.sync(rootPath); + } catch (error2) { + console.warn(error2.stack); // eslint-disable-line no-console + } + + throw 'fileIsNotImage'; + } + + try { + rimraf.sync(inputs.file.fd); + } catch (error) { + console.warn(error.stack); // eslint-disable-line no-console + } + + return { + dirname, + }; + }, +}; diff --git a/server/api/helpers/utils/create-attachment-receiver.js b/server/api/helpers/utils/create-attachment-receiver.js deleted file mode 100644 index 31384384..00000000 --- a/server/api/helpers/utils/create-attachment-receiver.js +++ /dev/null @@ -1,102 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const util = require('util'); -const stream = require('stream'); -const streamToArray = require('stream-to-array'); -const filenamify = require('filenamify'); -const { v4: uuid } = require('uuid'); -const sharp = require('sharp'); - -const writeFile = util.promisify(fs.writeFile); - -module.exports = { - sync: true, - - fn() { - const receiver = stream.Writable({ - objectMode: true, - }); - - let firstFileHandled = false; - // eslint-disable-next-line no-underscore-dangle - receiver._write = async (file, receiverEncoding, done) => { - if (firstFileHandled) { - file.pipe(new stream.Writable()); - - return done(); - } - firstFileHandled = true; - - const buffer = await streamToArray(file).then((parts) => - Buffer.concat(parts.map((part) => (Buffer.isBuffer(part) ? part : Buffer.from(part)))), - ); - - try { - const dirname = uuid(); - - // FIXME: https://github.com/sindresorhus/filenamify/issues/13 - const filename = filenamify(file.filename); - - const rootPath = path.join(sails.config.custom.attachmentsPath, dirname); - fs.mkdirSync(rootPath); - - await writeFile(path.join(rootPath, filename), buffer); - - const image = sharp(buffer); - let metadata; - - try { - metadata = await image.metadata(); - } catch (error) {} // eslint-disable-line no-empty - - const extra = { - dirname, - name: file.filename, - }; - - if (metadata && !['svg', 'pdf'].includes(metadata.format)) { - let cover256Buffer; - if (metadata.height > metadata.width) { - cover256Buffer = await image - .resize(256, 320) - .jpeg({ - quality: 100, - chromaSubsampling: '4:4:4', - }) - .toBuffer(); - } else { - cover256Buffer = await image - .resize({ - width: 256, - }) - .jpeg({ - quality: 100, - chromaSubsampling: '4:4:4', - }) - .toBuffer(); - } - - const thumbnailsPath = path.join(rootPath, 'thumbnails'); - fs.mkdirSync(thumbnailsPath); - - await writeFile(path.join(thumbnailsPath, 'cover-256.jpg'), cover256Buffer); - - extra.image = _.pick(metadata, ['width', 'height']); - } else { - extra.image = null; - } - - Object.assign(file, { - filename, - extra, - }); - - return done(); - } catch (error) { - return done(error); - } - }; - - return receiver; - }, -}; diff --git a/server/api/helpers/utils/create-project-background-image-receiver.js b/server/api/helpers/utils/create-project-background-image-receiver.js deleted file mode 100644 index e39fe92f..00000000 --- a/server/api/helpers/utils/create-project-background-image-receiver.js +++ /dev/null @@ -1,65 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const util = require('util'); -const stream = require('stream'); -const streamToArray = require('stream-to-array'); -const { v4: uuid } = require('uuid'); -const sharp = require('sharp'); - -const writeFile = util.promisify(fs.writeFile); - -module.exports = { - sync: true, - - fn() { - const receiver = stream.Writable({ - objectMode: true, - }); - - let firstFileHandled = false; - // eslint-disable-next-line no-underscore-dangle - receiver._write = async (file, receiverEncoding, done) => { - if (firstFileHandled) { - file.pipe(new stream.Writable()); - - return done(); - } - firstFileHandled = true; - - const buffer = await streamToArray(file).then((parts) => - Buffer.concat(parts.map((part) => (Buffer.isBuffer(part) ? part : Buffer.from(part)))), - ); - - try { - const originalBuffer = await sharp(buffer).jpeg().toBuffer(); - - const cover336Buffer = await sharp(buffer) - .resize(336, 200) - .jpeg({ - quality: 100, - chromaSubsampling: '4:4:4', - }) - .toBuffer(); - - const dirname = uuid(); - - const rootPath = path.join(sails.config.custom.projectBackgroundImagesPath, dirname); - fs.mkdirSync(rootPath); - - await writeFile(path.join(rootPath, 'original.jpg'), originalBuffer); - await writeFile(path.join(rootPath, 'cover-336.jpg'), cover336Buffer); - - // eslint-disable-next-line no-param-reassign - file.extra = { - dirname, - }; - - return done(); - } catch (error) { - return done(error); - } - }; - - return receiver; - }, -}; diff --git a/server/api/helpers/utils/create-user-avatar-receiver.js b/server/api/helpers/utils/create-user-avatar-receiver.js deleted file mode 100644 index 0c17f67c..00000000 --- a/server/api/helpers/utils/create-user-avatar-receiver.js +++ /dev/null @@ -1,70 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const util = require('util'); -const stream = require('stream'); -const streamToArray = require('stream-to-array'); -const { v4: uuid } = require('uuid'); -const sharp = require('sharp'); - -const writeFile = util.promisify(fs.writeFile); - -module.exports = { - sync: true, - - fn() { - const receiver = stream.Writable({ - objectMode: true, - }); - - let firstFileHandled = false; - // eslint-disable-next-line no-underscore-dangle - receiver._write = async (file, receiverEncoding, done) => { - if (firstFileHandled) { - file.pipe(new stream.Writable()); - - return done(); - } - firstFileHandled = true; - - const buffer = await streamToArray(file).then((parts) => - Buffer.concat(parts.map((part) => (Buffer.isBuffer(part) ? part : Buffer.from(part)))), - ); - - try { - const originalBuffer = await sharp(buffer) - .jpeg({ - quality: 100, - chromaSubsampling: '4:4:4', - }) - .toBuffer(); - - const square100Buffer = await sharp(buffer) - .resize(100, 100) - .jpeg({ - quality: 100, - chromaSubsampling: '4:4:4', - }) - .toBuffer(); - - const dirname = uuid(); - - const rootPath = path.join(sails.config.custom.userAvatarsPath, dirname); - fs.mkdirSync(rootPath); - - await writeFile(path.join(rootPath, 'original.jpg'), originalBuffer); - await writeFile(path.join(rootPath, 'square-100.jpg'), square100Buffer); - - // eslint-disable-next-line no-param-reassign - file.extra = { - dirname, - }; - - return done(); - } catch (error) { - return done(error); - } - }; - - return receiver; - }, -};