Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 13 additions & 16 deletions functions/v2/imagemagick/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

// [START functions_imagemagick_setup]
const functions = require('@google-cloud/functions-framework');
const gm = require('gm').subClass({imageMagick: true});
const sharp = require('sharp');
const fs = require('fs').promises;
const path = require('path');
const vision = require('@google-cloud/vision');
Expand Down Expand Up @@ -64,6 +64,7 @@ functions.cloudEvent('blurOffensiveImages', async cloudEvent => {
// Blurs the given file using ImageMagick, and uploads it to another bucket.
const blurImage = async (file, blurredBucketName) => {
const tempLocalPath = `/tmp/${path.parse(file.name).base}`;
const tempLocalBlurredPath = `/tmp/blurred-${path.parse(file.name).base}`;

// Download file from bucket.
try {
Expand All @@ -74,33 +75,29 @@ const blurImage = async (file, blurredBucketName) => {
throw new Error(`File download failed: ${err}`);
}

await new Promise((resolve, reject) => {
gm(tempLocalPath)
.blur(0, 16)
.write(tempLocalPath, (err, stdout) => {
if (err) {
console.error('Failed to blur image.', err);
reject(err);
} else {
console.log(`Blurred image: ${file.name}`);
resolve(stdout);
}
});
});
try {
await sharp(tempLocalPath).blur(16).toFile(tempLocalBlurredPath);

console.log(`Blurred image: ${file.name}`);
} catch (err) {
console.error('Failed to blur image.', err);
throw err;
}

// Upload result to a different bucket, to avoid re-triggering this function.
const blurredBucket = storage.bucket(blurredBucketName);

// Upload the Blurred image back into the bucket.
const gcsPath = `gs://${blurredBucketName}/${file.name}`;
try {
await blurredBucket.upload(tempLocalPath, {destination: file.name});
await blurredBucket.upload(tempLocalBlurredPath, {destination: file.name});
console.log(`Uploaded blurred image to: ${gcsPath}`);
} catch (err) {
throw new Error(`Unable to upload blurred image to ${gcsPath}: ${err}`);
}

// Delete the temporary file.
return fs.unlink(tempLocalPath);
await fs.unlink(tempLocalPath);
return await fs.unlink(tempLocalBlurredPath);
Comment on lines +78 to +101
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Temporary files created in the /tmp directory should be deleted even if an error occurs during processing. Currently, if sharp or upload fails, the unlink calls are skipped, which can lead to memory exhaustion in the Cloud Function environment. Wrapping the processing logic in a try...finally block ensures cleanup.

  try {
    try {
      await sharp(tempLocalPath).blur(16).toFile(tempLocalBlurredPath);
      console.log('Blurred image: ' + file.name);
    } catch (err) {
      console.error('Failed to blur image.', err);
      throw err;
    }

    // Upload result to a different bucket, to avoid re-triggering this function.
    const blurredBucket = storage.bucket(blurredBucketName);

    // Upload the Blurred image back into the bucket.
    const gcsPath = 'gs://' + blurredBucketName + '/' + file.name;
    try {
      await blurredBucket.upload(tempLocalBlurredPath, {destination: file.name});
      console.log('Uploaded blurred image to: ' + gcsPath);
    } catch (err) {
      throw new Error('Unable to upload blurred image to ' + gcsPath + ': ' + err);
    }
  } finally {
    // Delete the temporary files.
    await Promise.allSettled([
      fs.unlink(tempLocalPath),
      fs.unlink(tempLocalBlurredPath),
    ]);
  }

};
// [END functions_imagemagick_blur]
2 changes: 1 addition & 1 deletion functions/v2/imagemagick/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"@google-cloud/functions-framework": "^3.1.0",
"@google-cloud/storage": "^7.0.0",
"@google-cloud/vision": "^4.0.0",
"gm": "^1.23.1"
"sharp": "^0.34.5"
},
"devDependencies": {
"c8": "^10.0.0",
Expand Down
6 changes: 0 additions & 6 deletions functions/v2/imagemagick/test/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
'use strict';

const assert = require('assert');
const {execSync} = require('child_process');
const {Storage} = require('@google-cloud/storage');
const sinon = require('sinon');
const supertest = require('supertest');
Expand All @@ -34,11 +33,6 @@ const testFiles = {

require('../index');

// ImageMagick is available by default in Cloud Run Functions environments
// https://cloud.google.com/functions/1stgendocs/tutorials/imagemagick-1st-gen.md#importing_dependencies
// Manually install it for testing only.
execSync('sudo apt-get install imagemagick -y');

describe('functions/imagemagick tests', () => {
before(async () => {
let exists;
Expand Down
19 changes: 9 additions & 10 deletions functions/v2/imagemagick/test/unit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,30 @@ const loadSample = (adultResult, fileName) => {
return {
bucket: sinon.stub().returnsThis(),
file: sinon.stub().returnsThis(),
upload: sinon.stub().returnsThis(),
download: sinon.stub().returnsThis(),
upload: sinon.stub().resolves(),
download: sinon.stub().resolves(),
name: fileName,
};
},
};

const gm = () => {
return {
blur: sinon.stub().returnsThis(),
write: sinon.stub().yields(),
};
// 1. Reemplazamos la simulación de 'gm' por la de 'sharp'
const sharpInstance = {
blur: sinon.stub().returnsThis(),
toFile: sinon.stub().resolves(),
};
gm.subClass = sinon.stub().returnsThis();
const sharpMock = sinon.stub().returns(sharpInstance);

const fs = {
promises: {
unlink: sinon.stub(),
unlink: sinon.stub().resolves(),
},
};

return proxyquire('..', {
'@google-cloud/vision': vision,
'@google-cloud/storage': storage,
gm: gm,
sharp: sharpMock,
fs: fs,
});
};
Expand Down
Loading