diff --git a/docker-compose.yml b/docker-compose.yml index 1f586c44..eacc9f3d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -82,12 +82,12 @@ services: # - TELEGRAM_THREAD_ID= # Attachments S3 - # - ATTACHMENTS_S3=true - # - ATTACHMENTS_S3_REGION= - # - ATTACHMENTS_S3_ENDPOINT= - # - ATTACHMENTS_S3_BUCKET= - # - ATTACHMENTS_S3_ACCESS_KEY= - # - ATTACHMENTS_S3_SECRET_KEY= + # - S3_ENABLE=true + # - S3_REGION= + # - S3_ENDPOINT= + # - S3_BUCKET= + # - S3_ACCESS_KEY= + # - S3_SECRET_KEY= depends_on: postgres: condition: service_healthy diff --git a/server/.env.sample b/server/.env.sample index 3d30a14b..e9f3ec22 100644 --- a/server/.env.sample +++ b/server/.env.sample @@ -76,9 +76,9 @@ SECRET_KEY=notsecretkey TZ=UTC -# ATTACHMENTS_S3=true -# ATTACHMENTS_S3_REGION= -# ATTACHMENTS_S3_ENDPOINT= -# ATTACHMENTS_S3_BUCKET= -# ATTACHMENTS_S3_ACCESS_KEY= -# ATTACHMENTS_S3_SECRET_KEY= +# S3_ENABLE=true +# S3_REGION= +# S3_ENDPOINT= +# S3_BUCKET= +# S3_ACCESS_KEY= +# S3_SECRET_KEY= diff --git a/server/api/helpers/attachments/delete-one.js b/server/api/helpers/attachments/delete-one.js index afba7593..39eb4013 100644 --- a/server/api/helpers/attachments/delete-one.js +++ b/server/api/helpers/attachments/delete-one.js @@ -53,19 +53,26 @@ module.exports = { try { const type = attachment.type || 'local'; if (type === 's3') { - const client = await sails.helpers.attachments.getSimpleStorageServiceClient(); + const client = await sails.helpers.utils.getSimpleStorageServiceClient(); if (client) { - const file1 = `${attachment.dirname}/${attachment.filename}`; - const file2 = `${attachment.dirname}/thumbnails/cover-256.png`; - await client.delete({ Key: file1 }); - await client.delete({ Key: file2 }); + if (attachment.url) { + const parsedUrl = new URL(attachment.url); + await client.delete({ Key: parsedUrl.pathname.replace(/^\/+/, '') }); + } + if (attachment.thumb) { + const parsedUrl = new URL(attachment.thumb); + await client.delete({ Key: parsedUrl.pathname.replace(/^\/+/, '') }); + } } - } else { - rimraf.sync(path.join(sails.config.custom.attachmentsPath, attachment.dirname)); } } catch (error) { console.warn(error.stack); // eslint-disable-line no-console } + try { + rimraf.sync(path.join(sails.config.custom.attachmentsPath, attachment.dirname)); + } catch (error) { + console.warn(error.stack); // eslint-disable-line no-console + } sails.sockets.broadcast( `board:${inputs.board.id}`, diff --git a/server/api/helpers/attachments/process-uploaded-file.js b/server/api/helpers/attachments/process-uploaded-file.js index 93e443f0..92b6b443 100644 --- a/server/api/helpers/attachments/process-uploaded-file.js +++ b/server/api/helpers/attachments/process-uploaded-file.js @@ -23,11 +23,11 @@ module.exports = { const rootPath = path.join(sails.config.custom.attachmentsPath, dirname); const filePath = path.join(rootPath, filename); - if (sails.config.custom.attachmentsS3) { - const client = await sails.helpers.attachments.getSimpleStorageServiceClient(); + if (sails.config.custom.s3Config) { + const client = await sails.helpers.utils.getSimpleStorageServiceClient(); const s3Image = await client.upload({ Body: fs.createReadStream(inputs.file.fd), - Key: `${dirname}/${filename}`, + Key: `attachments/${dirname}/${filename}`, ContentType: inputs.file.type, }); @@ -72,7 +72,7 @@ module.exports = { ) .toBuffer(); const s3Thumb = await client.upload({ - Key: `${dirname}/thumbnails/cover-256.${thumbnailsExtension}`, + Key: `attachments/${dirname}/thumbnails/cover-256.${thumbnailsExtension}`, Body: resizeBuffer, ContentType: inputs.file.type, }); @@ -83,6 +83,12 @@ module.exports = { } } + try { + rimraf.sync(inputs.file.fd); + } catch (error) { + console.warn(error.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 index 70792a03..daecaee1 100644 --- a/server/api/helpers/projects/process-uploaded-background-image-file.js +++ b/server/api/helpers/projects/process-uploaded-background-image-file.js @@ -33,9 +33,6 @@ module.exports = { } const dirname = uuid(); - const rootPath = path.join(sails.config.custom.projectBackgroundImagesPath, dirname); - - fs.mkdirSync(rootPath); let { width, pageHeight: height = metadata.height } = metadata; if (metadata.orientation && metadata.orientation > 4) { @@ -44,6 +41,64 @@ module.exports = { const extension = metadata.format === 'jpeg' ? 'jpg' : metadata.format; + if (sails.config.custom.s3Config) { + const client = await sails.helpers.utils.getSimpleStorageServiceClient(); + let originalUrl = ''; + let thumbUrl = ''; + + try { + const s3Original = await client.upload({ + Body: await image.toBuffer(), + Key: `project-background-images/${dirname}/original.${extension}`, + ContentType: inputs.file.type, + }); + originalUrl = s3Original.Location; + + const resizeBuffer = await image + .resize( + 336, + 200, + width < 336 || height < 200 + ? { + kernel: sharp.kernel.nearest, + } + : undefined, + ) + .toBuffer(); + const s3Thumb = await client.upload({ + Body: resizeBuffer, + Key: `project-background-images/${dirname}/cover-336.${extension}`, + ContentType: inputs.file.type, + }); + thumbUrl = s3Thumb.Location; + } catch (error1) { + try { + client.delete({ Key: `project-background-images/${dirname}/original.${extension}` }); + } 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, + extension, + original: originalUrl, + thumb: thumbUrl, + }; + } + + const rootPath = path.join(sails.config.custom.projectBackgroundImagesPath, dirname); + + fs.mkdirSync(rootPath); + try { await image.toFile(path.join(rootPath, `original.${extension}`)); diff --git a/server/api/helpers/projects/update-one.js b/server/api/helpers/projects/update-one.js index db3e9418..b8f277b5 100644 --- a/server/api/helpers/projects/update-one.js +++ b/server/api/helpers/projects/update-one.js @@ -86,6 +86,21 @@ module.exports = { (!project.backgroundImage || project.backgroundImage.dirname !== inputs.record.backgroundImage.dirname) ) { + try { + if (sails.config.custom.s3Config) { + const client = await sails.helpers.utils.getSimpleStorageServiceClient(); + if (client && inputs.record.backgroundImage && inputs.record.backgroundImage.original) { + const parsedUrl = new URL(inputs.record.backgroundImage.original); + await client.delete({ Key: parsedUrl.pathname.replace(/^\/+/, '') }); + } + if (client && inputs.record.backgroundImage && inputs.record.backgroundImage.thumb) { + const parsedUrl = new URL(inputs.record.backgroundImage.thumb); + await client.delete({ Key: parsedUrl.pathname.replace(/^\/+/, '') }); + } + } + } catch (error) { + console.warn(error.stack); // eslint-disable-line no-console + } try { rimraf.sync( path.join( diff --git a/server/api/helpers/users/process-uploaded-avatar-file.js b/server/api/helpers/users/process-uploaded-avatar-file.js index 23058412..fc1b4a71 100644 --- a/server/api/helpers/users/process-uploaded-avatar-file.js +++ b/server/api/helpers/users/process-uploaded-avatar-file.js @@ -33,9 +33,6 @@ module.exports = { } const dirname = uuid(); - const rootPath = path.join(sails.config.custom.userAvatarsPath, dirname); - - fs.mkdirSync(rootPath); let { width, pageHeight: height = metadata.height } = metadata; if (metadata.orientation && metadata.orientation > 4) { @@ -44,6 +41,64 @@ module.exports = { const extension = metadata.format === 'jpeg' ? 'jpg' : metadata.format; + if (sails.config.custom.s3Config) { + const client = await sails.helpers.utils.getSimpleStorageServiceClient(); + let originalUrl = ''; + let squareUrl = ''; + + try { + const s3Original = await client.upload({ + Body: await image.toBuffer(), + Key: `user-avatars/${dirname}/original.${extension}`, + ContentType: inputs.file.type, + }); + originalUrl = s3Original.Location; + + const resizeBuffer = await image + .resize( + 100, + 100, + width < 100 || height < 100 + ? { + kernel: sharp.kernel.nearest, + } + : undefined, + ) + .toBuffer(); + const s3Square = await client.upload({ + Body: resizeBuffer, + Key: `user-avatars/${dirname}/square-100.${extension}`, + ContentType: inputs.file.type, + }); + squareUrl = s3Square.Location; + } catch (error1) { + try { + client.delete({ Key: `user-avatars/${dirname}/original.${extension}` }); + } 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, + extension, + original: originalUrl, + square: squareUrl, + }; + } + + const rootPath = path.join(sails.config.custom.userAvatarsPath, dirname); + + fs.mkdirSync(rootPath); + try { await image.toFile(path.join(rootPath, `original.${extension}`)); diff --git a/server/api/helpers/users/update-one.js b/server/api/helpers/users/update-one.js index 05f025f7..01aa819f 100644 --- a/server/api/helpers/users/update-one.js +++ b/server/api/helpers/users/update-one.js @@ -101,6 +101,21 @@ module.exports = { inputs.record.avatar && (!user.avatar || user.avatar.dirname !== inputs.record.avatar.dirname) ) { + try { + if (sails.config.custom.s3Config) { + const client = await sails.helpers.utils.getSimpleStorageServiceClient(); + if (client && inputs.record.avatar && inputs.record.avatar.original) { + const parsedUrl = new URL(inputs.record.avatar.original); + await client.delete({ Key: parsedUrl.pathname.replace(/^\/+/, '') }); + } + if (client && inputs.record.avatar && inputs.record.avatar.square) { + const parsedUrl = new URL(inputs.record.avatar.square); + await client.delete({ Key: parsedUrl.pathname.replace(/^\/+/, '') }); + } + } + } catch (error) { + console.warn(error.stack); // eslint-disable-line no-console + } try { rimraf.sync(path.join(sails.config.custom.userAvatarsPath, inputs.record.avatar.dirname)); } catch (error) { diff --git a/server/api/helpers/attachments/get-simple-storage-service-client.js b/server/api/helpers/utils/get-simple-storage-service-client.js similarity index 87% rename from server/api/helpers/attachments/get-simple-storage-service-client.js rename to server/api/helpers/utils/get-simple-storage-service-client.js index b6502044..649fcd4e 100644 --- a/server/api/helpers/attachments/get-simple-storage-service-client.js +++ b/server/api/helpers/utils/get-simple-storage-service-client.js @@ -37,8 +37,8 @@ class S3Client { module.exports = { fn() { - if (sails.config.custom.attachmentsS3) { - return new S3Client(sails.config.custom.attachmentsS3); + if (sails.config.custom.s3Config) { + return new S3Client(sails.config.custom.s3Config); } return null; }, diff --git a/server/api/models/Attachment.js b/server/api/models/Attachment.js index fb09f220..0beeca0a 100644 --- a/server/api/models/Attachment.js +++ b/server/api/models/Attachment.js @@ -31,9 +31,11 @@ module.exports = { }, url: { type: 'string', + allowNull: true, }, thumb: { type: 'string', + allowNull: true, }, // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗ diff --git a/server/api/models/Project.js b/server/api/models/Project.js index 58646370..ad13385e 100755 --- a/server/api/models/Project.js +++ b/server/api/models/Project.js @@ -79,11 +79,26 @@ module.exports = { }, customToJSON() { + let url = ''; + let coverUrl = ''; + if (this.backgroundImage) { + if (this.backgroundImage.original) { + url = this.backgroundImage.original; + } else { + url = `${sails.config.custom.projectBackgroundImagesUrl}/${this.backgroundImage.dirname}/original.${this.backgroundImage.extension}`; + } + if (this.backgroundImage.thumb) { + coverUrl = this.backgroundImage.thumb; + } else { + coverUrl = `${sails.config.custom.projectBackgroundImagesUrl}/${this.backgroundImage.dirname}/cover-336.${this.backgroundImage.extension}`; + } + } + return { ..._.omit(this, ['backgroundImage']), backgroundImage: this.backgroundImage && { - url: `${sails.config.custom.projectBackgroundImagesUrl}/${this.backgroundImage.dirname}/original.${this.backgroundImage.extension}`, - coverUrl: `${sails.config.custom.projectBackgroundImagesUrl}/${this.backgroundImage.dirname}/cover-336.${this.backgroundImage.extension}`, + url, + coverUrl, }, }; }, diff --git a/server/api/models/User.js b/server/api/models/User.js index a3336d39..4421d1ec 100755 --- a/server/api/models/User.js +++ b/server/api/models/User.js @@ -148,6 +148,14 @@ module.exports = { customToJSON() { const isDefaultAdmin = this.email === sails.config.custom.defaultAdminEmail; + let avatarUrl = ''; + if (this.avatar) { + if (this.avatar.square) { + avatarUrl = this.avatar.square; + } else { + avatarUrl = `${sails.config.custom.userAvatarsUrl}/${this.avatar.dirname}/square-100.${this.avatar.extension}`; + } + } return { ..._.omit(this, ['password', 'isSso', 'avatar', 'passwordChangedAt']), @@ -155,9 +163,7 @@ module.exports = { isRoleLocked: (this.isSso && !sails.config.custom.oidcIgnoreRoles) || isDefaultAdmin, isUsernameLocked: (this.isSso && !sails.config.custom.oidcIgnoreUsername) || isDefaultAdmin, isDeletionLocked: isDefaultAdmin, - avatarUrl: - this.avatar && - `${sails.config.custom.userAvatarsUrl}/${this.avatar.dirname}/square-100.${this.avatar.extension}`, + avatarUrl, }; }, }; diff --git a/server/config/custom.js b/server/config/custom.js index fd5e6b2a..4c4ad1e5 100644 --- a/server/config/custom.js +++ b/server/config/custom.js @@ -36,19 +36,20 @@ module.exports.custom = { projectBackgroundImagesPath: path.join(sails.config.paths.public, 'project-background-images'), projectBackgroundImagesUrl: `${process.env.BASE_URL}/project-background-images`, - attachmentsS3: - process.env.ATTACHMENTS_S3 === 'true' - ? { - accessKeyId: process.env.ATTACHMENTS_S3_ACCESS_KEY, - secretAccessKey: process.env.ATTACHMENTS_S3_SECRET_KEY, - region: process.env.ATTACHMENTS_S3_REGION, - endpoint: process.env.ATTACHMENTS_S3_ENDPOINT, - bucket: process.env.ATTACHMENTS_S3_BUCKET, - } - : null, attachmentsPath: path.join(sails.config.appPath, 'private', 'attachments'), attachmentsUrl: `${process.env.BASE_URL}/attachments`, + s3Config: + process.env.S3_ENABLE === 'true' + ? { + accessKeyId: process.env.S3_ACCESS_KEY, + secretAccessKey: process.env.S3_SECRET_KEY, + region: process.env.S3_REGION, + endpoint: process.env.S3_ENDPOINT, + bucket: process.env.S3_BUCKET, + } + : null, + defaultAdminEmail: process.env.DEFAULT_ADMIN_EMAIL && process.env.DEFAULT_ADMIN_EMAIL.toLowerCase(),