diff --git a/server/api/helpers/attachments/process-uploaded-file.js b/server/api/helpers/attachments/process-uploaded-file.js index 624ac63f..a9d4f4d9 100644 --- a/server/api/helpers/attachments/process-uploaded-file.js +++ b/server/api/helpers/attachments/process-uploaded-file.js @@ -87,40 +87,41 @@ module.exports = { const thumbnailsPathSegment = `${dirPathSegment}/thumbnails`; const thumbnailsExtension = metadata.format === 'jpeg' ? 'jpg' : metadata.format; + const outside360 = image + .clone() + .resize(360, 360, { + fit: 'outside', + withoutEnlargement: true, + }) + .png({ + quality: 75, + force: false, + }); + + const outside720 = image + .clone() + .resize(720, 720, { + fit: 'outside', + withoutEnlargement: true, + }) + .png({ + quality: 75, + force: false, + }); + try { - const outside360Buffer = await image - .resize(360, 360, { - fit: 'outside', - withoutEnlargement: true, - }) - .png({ - quality: 75, - force: false, - }) - .toBuffer(); - - await fileManager.save( - `${thumbnailsPathSegment}/outside-360.${thumbnailsExtension}`, - outside360Buffer, - inputs.file.type, - ); - - const outside720Buffer = await image - .resize(720, 720, { - fit: 'outside', - withoutEnlargement: true, - }) - .png({ - quality: 75, - force: false, - }) - .toBuffer(); - - await fileManager.save( - `${thumbnailsPathSegment}/outside-720.${thumbnailsExtension}`, - outside720Buffer, - inputs.file.type, - ); + await Promise.all([ + fileManager.save( + `${thumbnailsPathSegment}/outside-360.${thumbnailsExtension}`, + outside360, + inputs.file.type, + ), + fileManager.save( + `${thumbnailsPathSegment}/outside-720.${thumbnailsExtension}`, + outside720, + inputs.file.type, + ), + ]); data.image = { width, diff --git a/server/api/helpers/background-images/process-uploaded-file.js b/server/api/helpers/background-images/process-uploaded-file.js index 2812d060..b1aa68ae 100644 --- a/server/api/helpers/background-images/process-uploaded-file.js +++ b/server/api/helpers/background-images/process-uploaded-file.js @@ -39,21 +39,17 @@ module.exports = { }); let metadata; - let originalBuffer; - try { metadata = await image.metadata(); - - if (metadata.orientation && metadata.orientation > 4) { - image = image.rotate(); - } - - originalBuffer = await image.toBuffer(); } catch (error) { await rimraf(inputs.file.fd); throw 'fileIsNotImage'; } + if (metadata.orientation && metadata.orientation > 4) { + image = image.rotate(); + } + const { id: uploadedFileId } = await UploadedFile.qm.createOne({ mimeType, size, @@ -64,29 +60,26 @@ module.exports = { const dirPathSegment = `${sails.config.custom.backgroundImagesPathSegment}/${uploadedFileId}`; const extension = metadata.format === 'jpeg' ? 'jpg' : metadata.format; + const outside360 = image + .clone() + .resize(360, 360, { + fit: 'outside', + withoutEnlargement: true, + }) + .png({ + quality: 75, + force: false, + }); + try { - await fileManager.save( - `${dirPathSegment}/original.${extension}`, - originalBuffer, - inputs.file.type, - ); - - const outside360Buffer = await image - .resize(360, 360, { - fit: 'outside', - withoutEnlargement: true, - }) - .png({ - quality: 75, - force: false, - }) - .toBuffer(); - - await fileManager.save( - `${dirPathSegment}/outside-360.${extension}`, - outside360Buffer, - inputs.file.type, - ); + await Promise.all([ + fileManager.save(`${dirPathSegment}/original.${extension}`, image, inputs.file.type), + fileManager.save( + `${dirPathSegment}/outside-360.${extension}`, + outside360, + inputs.file.type, + ), + ]); } catch (error) { sails.log.warn(error.stack); diff --git a/server/api/helpers/users/process-uploaded-avatar-file.js b/server/api/helpers/users/process-uploaded-avatar-file.js index 28fe01a4..d2b5ead3 100644 --- a/server/api/helpers/users/process-uploaded-avatar-file.js +++ b/server/api/helpers/users/process-uploaded-avatar-file.js @@ -39,21 +39,17 @@ module.exports = { }); let metadata; - let originalBuffer; - try { metadata = await image.metadata(); - - if (metadata.orientation && metadata.orientation > 4) { - image = image.rotate(); - } - - originalBuffer = await image.toBuffer(); } catch (error) { await rimraf(inputs.file.fd); throw 'fileIsNotImage'; } + if (metadata.orientation && metadata.orientation > 4) { + image = image.rotate(); + } + const { id: uploadedFileId } = await UploadedFile.qm.createOne({ mimeType, size, @@ -64,28 +60,21 @@ module.exports = { const dirPathSegment = `${sails.config.custom.userAvatarsPathSegment}/${uploadedFileId}`; const extension = metadata.format === 'jpeg' ? 'jpg' : metadata.format; + const cover180 = image + .clone() + .resize(180, 180, { + withoutEnlargement: true, + }) + .png({ + quality: 75, + force: false, + }); + try { - await fileManager.save( - `${dirPathSegment}/original.${extension}`, - originalBuffer, - inputs.file.type, - ); - - const cover180Buffer = await image - .resize(180, 180, { - withoutEnlargement: true, - }) - .png({ - quality: 75, - force: false, - }) - .toBuffer(); - - await fileManager.save( - `${dirPathSegment}/cover-180.${extension}`, - cover180Buffer, - inputs.file.type, - ); + await Promise.all([ + fileManager.save(`${dirPathSegment}/original.${extension}`, image, inputs.file.type), + fileManager.save(`${dirPathSegment}/cover-180.${extension}`, cover180, inputs.file.type), + ]); } catch (error) { sails.log.warn(error.stack); diff --git a/server/api/helpers/utils/download-favicon.js b/server/api/helpers/utils/download-favicon.js index a907f68b..89fec468 100644 --- a/server/api/helpers/utils/download-favicon.js +++ b/server/api/helpers/utils/download-favicon.js @@ -162,23 +162,22 @@ module.exports = { const fileManager = sails.hooks['file-manager'].getInstance(); const { width, height } = metadata; - try { - const buffer = await image - .resize( - 32, - 32, - width < 32 || height < 32 - ? { - kernel: sharp.kernel.nearest, - } - : undefined, - ) - .png() - .toBuffer(); + image = image + .resize( + 32, + 32, + width < 32 || height < 32 + ? { + kernel: sharp.kernel.nearest, + } + : undefined, + ) + .png(); + try { await fileManager.save( `${sails.config.custom.faviconsPathSegment}/${hostname}.png`, - buffer, + image, 'image/png', ); } catch (error) { diff --git a/server/api/hooks/file-manager/LocalFileManager.js b/server/api/hooks/file-manager/LocalFileManager.js index 2b4d7f21..ee55643f 100644 --- a/server/api/hooks/file-manager/LocalFileManager.js +++ b/server/api/hooks/file-manager/LocalFileManager.js @@ -6,6 +6,7 @@ const fs = require('fs'); const fse = require('fs-extra'); const path = require('path'); +const { pipeline } = require('stream/promises'); const { rimraf } = require('rimraf'); const PATH_SEGMENT_TO_URL_REPLACE_REGEX = /(public|private)\//; @@ -27,8 +28,12 @@ class LocalFileManager { } // eslint-disable-next-line class-methods-use-this - async save(filePathSegment, buffer) { - await fse.outputFile(buildPath(filePathSegment), buffer); + async save(filePathSegment, stream) { + const filePath = buildPath(filePathSegment); + const { dir: dirPath } = path.parse(filePath); + + await fs.promises.mkdir(dirPath, { recursive: true }); + await pipeline(stream, fs.createWriteStream(filePath)); } // eslint-disable-next-line class-methods-use-this diff --git a/server/api/hooks/file-manager/S3FileManager.js b/server/api/hooks/file-manager/S3FileManager.js index 50738736..740fa25d 100644 --- a/server/api/hooks/file-manager/S3FileManager.js +++ b/server/api/hooks/file-manager/S3FileManager.js @@ -13,6 +13,7 @@ const { ListObjectsV2Command, PutObjectCommand, } = require('@aws-sdk/client-s3'); +const { Upload } = require('@aws-sdk/lib-storage'); class S3FileManager { constructor(client) { @@ -31,15 +32,18 @@ class S3FileManager { return null; } - async save(filePathSegment, buffer, contentType) { - const command = new PutObjectCommand({ - Bucket: sails.config.custom.s3Bucket, - Key: filePathSegment, - Body: buffer, - ContentType: contentType, + async save(filePathSegment, stream, contentType) { + const upload = new Upload({ + client: this.client, + params: { + Bucket: sails.config.custom.s3Bucket, + Key: filePathSegment, + Body: stream, + ContentType: contentType, + }, }); - await this.client.send(command); + await upload.done(); } async read(filePathSegment) { diff --git a/server/package-lock.json b/server/package-lock.json index 355355af..19daa799 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -8,6 +8,7 @@ "hasInstallScript": true, "dependencies": { "@aws-sdk/client-s3": "3.726.1", + "@aws-sdk/lib-storage": "3.726.1", "bcrypt": "^5.1.1", "bytes": "^3.1.2", "cross-env": "^7.0.3", @@ -695,6 +696,27 @@ "@aws-sdk/client-sts": "^3.723.0" } }, + "node_modules/@aws-sdk/lib-storage": { + "version": "3.726.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.726.1.tgz", + "integrity": "sha512-WuDxSZ8Bfe1N7gn5eXQ02dhlKWCAwW5qQErpJ4CCddXosF+gLxhGkrP9LkaaP0CpA3PxboHyET6HbWAggOWtqA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.0", + "@smithy/middleware-endpoint": "^4.0.0", + "@smithy/smithy-client": "^4.0.0", + "buffer": "5.6.0", + "events": "3.3.0", + "stream-browserify": "3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.726.1" + } + }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { "version": "3.726.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.726.0.tgz", @@ -3124,6 +3146,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -3280,6 +3322,16 @@ "dev": true, "license": "ISC" }, + "node_modules/buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -4930,6 +4982,15 @@ "node": ">= 0.6" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -10599,6 +10660,16 @@ "node": ">= 0.4" } }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, "node_modules/streamifier": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/streamifier/-/streamifier-0.1.1.tgz", diff --git a/server/package.json b/server/package.json index ac9499c8..713576cd 100644 --- a/server/package.json +++ b/server/package.json @@ -45,6 +45,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "3.726.1", + "@aws-sdk/lib-storage": "3.726.1", "bcrypt": "^5.1.1", "bytes": "^3.1.2", "cross-env": "^7.0.3",