diff --git a/.kokoro/build.sh b/.kokoro/build.sh index 8b91dc6512..167259647f 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -45,7 +45,12 @@ export SENDGRID_API_KEY=$(cat $KOKORO_GFILE_DIR/secrets-sendgrid-api-key.txt) export FUNCTIONS_TOPIC=integration-tests-instance export FUNCTIONS_BUCKET=$GCLOUD_PROJECT export OUTPUT_BUCKET=$FUNCTIONS_BUCKET + +# functions/translate export SUPPORTED_LANGUAGE_CODES="en,es" +export TRANSLATE_TOPIC=$FUNCTIONS_TOPIC +export RESULT_TOPIC=$FUNCTIONS_TOPIC +export RESULT_BUCKET=$FUNCTIONS_BUCKET # Configure IoT variables export NODEJS_IOT_EC_PUBLIC_KEY=${KOKORO_GFILE_DIR}/ec_public.pem diff --git a/functions/ocr/app/index.js b/functions/ocr/app/index.js index c685ad78cd..bf1fdfa1e5 100644 --- a/functions/ocr/app/index.js +++ b/functions/ocr/app/index.js @@ -1,5 +1,5 @@ /** - * Copyright 2016, Google, Inc. + * Copyright 2019, Google LLC. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -16,7 +16,12 @@ 'use strict'; // [START functions_ocr_setup] -const config = require('./config.json'); +const config = require('nconf') + .env() + .file('./config.json') + .defaults({ + TO_LANG: ['en', 'es'], + }); // Get a reference to the Pub/Sub component const {PubSub} = require('@google-cloud/pubsub'); @@ -43,14 +48,12 @@ const {Buffer} = require('safe-buffer'); * @param {string} topicName Name of the topic on which to publish. * @param {object} data The message data to publish. */ -function publishResult(topicName, data) { +const publishResult = async (topicName, data) => { const dataBuffer = Buffer.from(JSON.stringify(data)); - return pubsub - .topic(topicName) - .get({autoCreate: true}) - .then(([topic]) => topic.publish(dataBuffer)); -} + const [topic] = await pubsub.topic(topicName).get({autoCreate: true}); + topic.publish(dataBuffer); +}; // [END functions_ocr_publish] // [START functions_ocr_detect] @@ -61,42 +64,38 @@ function publishResult(topicName, data) { * @param {string} filename Cloud Storage file name. * @returns {Promise} */ -function detectText(bucketName, filename) { - let text; - +const detectText = async (bucketName, filename) => { console.log(`Looking for text in image ${filename}`); - return vision - .textDetection(`gs://${bucketName}/${filename}`) - .then(([detections]) => { - const annotation = detections.textAnnotations[0]; - text = annotation ? annotation.description : ''; - console.log(`Extracted text from image (${text.length} chars)`); - return translate.detect(text); - }) - .then(([detection]) => { - if (Array.isArray(detection)) { - detection = detection[0]; - } - console.log(`Detected language "${detection.language}" for ${filename}`); - - // Submit a message to the bus for each language we're going to translate to - const tasks = config.TO_LANG.map(lang => { - let topicName = config.TRANSLATE_TOPIC; - if (detection.language === lang) { - topicName = config.RESULT_TOPIC; - } - const messageData = { - text: text, - filename: filename, - lang: lang, - }; - - return publishResult(topicName, messageData); - }); - - return Promise.all(tasks); - }); -} + const [textDetections] = await vision.textDetection( + `gs://${bucketName}/${filename}` + ); + const [annotation] = textDetections.textAnnotations; + const text = annotation ? annotation.description : ''; + console.log(`Extracted text from image:`, text); + + let [translateDetection] = await translate.detect(text); + if (Array.isArray(translateDetection)) { + [translateDetection] = translateDetection; + } + console.log( + `Detected language "${translateDetection.language}" for ${filename}` + ); + + // Submit a message to the bus for each language we're going to translate to + const topicName = config.get('TRANSLATE_TOPIC'); + + const tasks = config.get('TO_LANG').map(lang => { + const messageData = { + text: text, + filename: filename, + lang: lang, + }; + + return publishResult(topicName, messageData); + }); + + return Promise.all(tasks); +}; // [END functions_ocr_detect] // [START functions_ocr_rename] @@ -107,9 +106,9 @@ function detectText(bucketName, filename) { * @param {string} lang Language to append. * @returns {string} The new filename. */ -function renameImageForSave(filename, lang) { +const renameImageForSave = (filename, lang) => { return `${filename}_to_${lang}.txt`; -} +}; // [END functions_ocr_rename] // [START functions_ocr_process] @@ -118,35 +117,24 @@ function renameImageForSave(filename, lang) { * a file is uploaded to the Cloud Storage bucket you created * for uploading images. * - * @param {object} event.data (Node 6) A Google Cloud Storage File object. - * @param {object} event (Node 8+) A Google Cloud Storage File object. + * @param {object} event A Google Cloud Storage File object. */ -exports.processImage = event => { - const file = event.data || event; - - return Promise.resolve() - .then(() => { - if (file.resourceState === 'not_exists') { - // This was a deletion event, we don't want to process this - return; - } - - if (!file.bucket) { - throw new Error( - 'Bucket not provided. Make sure you have a "bucket" property in your request' - ); - } - if (!file.name) { - throw new Error( - 'Filename not provided. Make sure you have a "name" property in your request' - ); - } - - return detectText(file.bucket, file.name); - }) - .then(() => { - console.log(`File ${file.name} processed.`); - }); +exports.processImage = async event => { + const {bucket, name} = event; + + if (!bucket) { + throw new Error( + 'Bucket not provided. Make sure you have a "bucket" property in your request' + ); + } + if (!name) { + throw new Error( + 'Filename not provided. Make sure you have a "name" property in your request' + ); + } + + await detectText(bucket, name); + console.log(`File ${name} processed.`); }; // [END functions_ocr_process] @@ -157,49 +145,44 @@ exports.processImage = event => { * by the TRANSLATE_TOPIC value in the config.json file. The * function translates text using the Google Translate API. * - * @param {object} event.data (Node 6) The Cloud Pub/Sub Message object. - * @param {object} event (Node 8+) The Cloud Pub/Sub Message object. + * @param {object} event The Cloud Pub/Sub Message object. * @param {string} {messageObject}.data The "data" property of the Cloud Pub/Sub * Message. This property will be a base64-encoded string that you must decode. */ -exports.translateText = event => { - const pubsubData = event.data.data || event.data; +exports.translateText = async event => { + const pubsubData = event.data; const jsonStr = Buffer.from(pubsubData, 'base64').toString(); - const payload = JSON.parse(jsonStr); - - return Promise.resolve() - .then(() => { - if (!payload.text) { - throw new Error( - 'Text not provided. Make sure you have a "text" property in your request' - ); - } - if (!payload.filename) { - throw new Error( - 'Filename not provided. Make sure you have a "filename" property in your request' - ); - } - if (!payload.lang) { - throw new Error( - 'Language not provided. Make sure you have a "lang" property in your request' - ); - } - - console.log(`Translating text into ${payload.lang}`); - return translate.translate(payload.text, payload.lang); - }) - .then(([translation]) => { - const messageData = { - text: translation, - filename: payload.filename, - lang: payload.lang, - }; - - return publishResult(config.RESULT_TOPIC, messageData); - }) - .then(() => { - console.log(`Text translated to ${payload.lang}`); - }); + const {text, filename, lang} = JSON.parse(jsonStr); + + if (!text) { + throw new Error( + 'Text not provided. Make sure you have a "text" property in your request' + ); + } + if (!filename) { + throw new Error( + 'Filename not provided. Make sure you have a "filename" property in your request' + ); + } + if (!lang) { + throw new Error( + 'Language not provided. Make sure you have a "lang" property in your request' + ); + } + + console.log(`Translating text into ${lang}`); + const [translation] = await translate.translate(text, lang); + + console.log(`Translated text:`, translation); + + const messageData = { + text: translation, + filename: filename, + lang: lang, + }; + + await publishResult(config.get('RESULT_TOPIC'), messageData); + console.log(`Text translated to ${lang}`); }; // [END functions_ocr_translate] @@ -210,46 +193,40 @@ exports.translateText = event => { * by the RESULT_TOPIC value in the config.json file. The * function saves the data packet to a file in GCS. * - * @param {object} event.data (Node 6) The Cloud Pub/Sub Message object. - * @param {object} event (Node 8+) The Cloud Pub/Sub Message object. + * @param {object} event The Cloud Pub/Sub Message object. * @param {string} {messageObject}.data The "data" property of the Cloud Pub/Sub * Message. This property will be a base64-encoded string that you must decode. */ -exports.saveResult = event => { - const pubsubData = event.data.data || event.data; +exports.saveResult = async event => { + const pubsubData = event.data; const jsonStr = Buffer.from(pubsubData, 'base64').toString(); - const payload = JSON.parse(jsonStr); - - return Promise.resolve() - .then(() => { - if (!payload.text) { - throw new Error( - 'Text not provided. Make sure you have a "text" property in your request' - ); - } - if (!payload.filename) { - throw new Error( - 'Filename not provided. Make sure you have a "filename" property in your request' - ); - } - if (!payload.lang) { - throw new Error( - 'Language not provided. Make sure you have a "lang" property in your request' - ); - } - - console.log(`Received request to save file ${payload.filename}`); - - const bucketName = config.RESULT_BUCKET; - const filename = renameImageForSave(payload.filename, payload.lang); - const file = storage.bucket(bucketName).file(filename); - - console.log(`Saving result to ${filename} in bucket ${bucketName}`); - - return file.save(payload.text); - }) - .then(() => { - console.log(`File saved.`); - }); + const {text, filename, lang} = JSON.parse(jsonStr); + + if (!text) { + throw new Error( + 'Text not provided. Make sure you have a "text" property in your request' + ); + } + if (!filename) { + throw new Error( + 'Filename not provided. Make sure you have a "filename" property in your request' + ); + } + if (!lang) { + throw new Error( + 'Language not provided. Make sure you have a "lang" property in your request' + ); + } + + console.log(`Received request to save file ${filename}`); + + const bucketName = config.get('RESULT_BUCKET'); + const newFilename = renameImageForSave(filename, lang); + const file = storage.bucket(bucketName).file(newFilename); + + console.log(`Saving result to ${newFilename} in bucket ${bucketName}`); + + await file.save(text); + console.log(`File saved.`); }; // [END functions_ocr_save] diff --git a/functions/ocr/app/package.json b/functions/ocr/app/package.json index fd9bb18cf4..d21cea3e35 100644 --- a/functions/ocr/app/package.json +++ b/functions/ocr/app/package.json @@ -19,16 +19,21 @@ "@google-cloud/storage": "^2.3.3", "@google-cloud/translate": "^3.0.0", "@google-cloud/vision": "^0.25.0", + "nconf": "^0.10.0", "safe-buffer": "^5.1.2" }, "devDependencies": { - "@google-cloud/nodejs-repo-tools": "^3.3.0", - "mocha": "^6.0.0", - "proxyquire": "^2.1.0", - "sinon": "^7.2.7" + "@google-cloud/nodejs-repo-tools": "^3.3.0", + "mocha": "^6.0.0" }, "cloud-repo-tools": { "requiresKeyFile": true, - "requiresProjectId": true + "requiresProjectId": true, + "requiredEnvVars": [ + "FUNCTIONS_BUCKET", + "OUTPUT_BUCKET", + "TRANSLATE_TOPIC", + "RESULT_TOPIC" + ] } } diff --git a/functions/ocr/app/test/index.test.js b/functions/ocr/app/test/index.test.js index dfabf5673c..5840e148cd 100644 --- a/functions/ocr/app/test/index.test.js +++ b/functions/ocr/app/test/index.test.js @@ -1,5 +1,5 @@ /** - * Copyright 2017, Google, Inc. + * Copyright 2019, Google LLC. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -15,232 +15,72 @@ 'use strict'; -const proxyquire = require('proxyquire').noCallThru(); -const sinon = require('sinon'); const assert = require('assert'); -const tools = require('@google-cloud/nodejs-repo-tools'); const {Buffer} = require('safe-buffer'); -const bucketName = 'my-bucket'; -const filename = 'image.jpg'; -const text = 'text'; -const lang = 'lang'; -const translation = 'translation'; - -function getSample() { - const config = { - RESULT_TOPIC: 'result-topic', - RESULT_BUCKET: 'result-bucket', - TRANSLATE_TOPIC: 'translate-topic', - TO_LANG: ['en', 'fr', 'es', 'ja', 'ru'], - }; - const topic = { - publish: sinon.stub().returns(Promise.resolve([])), - }; - topic.get = sinon.stub().returns(Promise.resolve([topic])); - topic.publisher = sinon.stub().returns(topic); - - const file = { - save: sinon.stub().returns(Promise.resolve([])), - bucket: bucketName, - name: filename, - }; - const bucket = { - file: sinon.stub().returns(file), - }; - const pubsubMock = { - topic: sinon.stub().returns(topic), - }; - const storageMock = { - bucket: sinon.stub().returns(bucket), - }; - const visionMock = { - textDetection: sinon - .stub() - .returns(Promise.resolve([{textAnnotations: [{description: text}]}])), - }; - const translateMock = { - detect: sinon.stub().returns(Promise.resolve([{language: 'ja'}])), - translate: sinon.stub().returns(Promise.resolve([translation])), - }; - - const PubsubMock = sinon.stub().returns(pubsubMock); - const StorageMock = sinon.stub().returns(storageMock); - - const stubConstructor = (packageName, property, mocks) => { - let stubInstance = sinon.createStubInstance( - require(packageName)[property], - mocks - ); - stubInstance = Object.assign(stubInstance, mocks); +const {Storage} = require('@google-cloud/storage'); +const storage = new Storage(); - const out = {}; - out[property] = sinon.stub().returns(stubInstance); - return out; - }; +const tools = require('@google-cloud/nodejs-repo-tools'); - const visionStub = stubConstructor( - '@google-cloud/vision', - 'ImageAnnotatorClient', - visionMock - ); - const translateStub = stubConstructor( - '@google-cloud/translate', - 'Translate', - translateMock - ); +const bucketName = process.env.FUNCTIONS_BUCKET; +const filename = 'wakeupcat.jpg'; +const text = 'Wake up human!'; +const lang = 'en'; - return { - program: proxyquire('../', { - '@google-cloud/translate': translateStub, - '@google-cloud/vision': visionStub, - '@google-cloud/pubsub': {PubSub: PubsubMock}, - '@google-cloud/storage': {Storage: StorageMock}, - './config.json': config, - }), - mocks: { - config, - pubsub: pubsubMock, - storage: storageMock, - bucket: bucket, - file, - vision: visionMock, - translate: translateMock, - topic, - }, - }; -} +const {RESULT_BUCKET} = process.env; -beforeEach(tools.stubConsole); -afterEach(tools.restoreConsole); +const program = require('..'); -it('processImage does nothing on delete', async () => { - await getSample().program.processImage({data: {resourceState: 'not_exists'}}); -}); +const errorMsg = (name, propertyName) => { + propertyName = propertyName || name.toLowerCase(); + return `${name} not provided. Make sure you have a "${propertyName}" property in your request`; +}; -it('processImage fails without a bucket', async () => { - const error = new Error( - 'Bucket not provided. Make sure you have a "bucket" property in your request' - ); - try { - await getSample().program.processImage({data: {}}); - } catch (err) { - assert.deepStrictEqual(err, error); - } -}); +before(tools.stubConsole); +after(tools.restoreConsole); -it('processImage fails without a name', async () => { - const error = new Error( - 'Filename not provided. Make sure you have a "name" property in your request' - ); - try { - await getSample().program.processImage({data: {bucket: bucketName}}); - } catch (err) { - assert.deepStrictEqual(err, error); - } -}); +describe('processImage', () => { + it('processImage validates parameters', async () => { + try { + await program.processImage({data: {}}); + assert.fail('no error thrown'); + } catch (err) { + assert.strictEqual(err.message, errorMsg('Bucket')); + } + }); -it('processImage processes an image with Node 6 arguments', async () => { - const event = { - data: { + it('processImage detects text', async () => { + const data = { bucket: bucketName, name: filename, - }, - }; - const sample = getSample(); - - await sample.program.processImage(event); - assert.strictEqual(console.log.callCount, 4); - assert.deepStrictEqual(console.log.getCall(0).args, [ - `Looking for text in image ${filename}`, - ]); - assert.deepStrictEqual(console.log.getCall(1).args, [ - `Extracted text from image (${text.length} chars)`, - ]); - assert.deepStrictEqual(console.log.getCall(2).args, [ - `Detected language "ja" for ${filename}`, - ]); - assert.deepStrictEqual(console.log.getCall(3).args, [ - `File ${event.data.name} processed.`, - ]); -}); + }; -it('processImage processes an image with Node 8 arguments', async () => { - const data = { - bucket: bucketName, - name: filename, - }; - const sample = getSample(); - - await sample.program.processImage(data); - assert.strictEqual(console.log.callCount, 4); - assert.deepStrictEqual(console.log.getCall(0).args, [ - `Looking for text in image ${filename}`, - ]); - assert.deepStrictEqual(console.log.getCall(1).args, [ - `Extracted text from image (${text.length} chars)`, - ]); - assert.deepStrictEqual(console.log.getCall(2).args, [ - `Detected language "ja" for ${filename}`, - ]); - assert.deepStrictEqual(console.log.getCall(3).args, [ - `File ${data.name} processed.`, - ]); + await program.processImage(data); + assert.ok(console.log.calledWith(`Detected language "en" for ${filename}`)); + assert.ok( + console.log.calledWith(`Extracted text from image:`, `${text}\n`) + ); + assert.ok(console.log.calledWith(`Detected language "en" for ${filename}`)); + assert.ok(console.log.calledWith(`File ${filename} processed.`)); + }); }); -it('translateText fails without text', async () => { - const error = new Error( - 'Text not provided. Make sure you have a "text" property in your request' - ); - const event = { - data: { +describe('translateText', () => { + it('translateText validates parameters', async () => { + const event = { data: Buffer.from(JSON.stringify({})).toString('base64'), - }, - }; - try { - await getSample().program.translateText(event); - } catch (err) { - assert.deepStrictEqual(err, error); - } -}); - -it('translateText fails without a filename', async () => { - const error = new Error( - 'Filename not provided. Make sure you have a "filename" property in your request' - ); - const event = { - data: { - data: Buffer.from(JSON.stringify({text})).toString('base64'), - }, - }; - - try { - await getSample().program.translateText(event); - } catch (err) { - assert.deepStrictEqual(err, error); - } -}); - -it('translateText fails without a lang', async () => { - const error = new Error( - 'Language not provided. Make sure you have a "lang" property in your request' - ); - const event = { - data: { - data: Buffer.from(JSON.stringify({text, filename})).toString('base64'), - }, - }; - - try { - await getSample().program.translateText(event); - } catch (err) { - assert.deepStrictEqual(err, error); - } -}); - -it('translateText translates and publishes text with Node 6 arguments', async () => { - const event = { - data: { + }; + try { + await program.translateText(event); + assert.fail('no error thrown'); + } catch (err) { + assert.deepStrictEqual(err.message, errorMsg('Text')); + } + }); + + it('translateText translates and publishes text', async () => { + const data = { data: Buffer.from( JSON.stringify({ text, @@ -248,154 +88,54 @@ it('translateText translates and publishes text with Node 6 arguments', async () lang, }) ).toString('base64'), - }, - }; - const sample = getSample(); - - sample.mocks.translate.translate.returns(Promise.resolve([translation])); + }; - await sample.program.translateText(event); - assert.strictEqual(console.log.callCount, 2); - assert.deepStrictEqual(console.log.firstCall.args, [ - `Translating text into ${lang}`, - ]); - assert.deepStrictEqual(console.log.secondCall.args, [ - `Text translated to ${lang}`, - ]); + await program.translateText(data); + assert.ok(console.log.calledWith(`Translating text into ${lang}`)); + assert.ok(console.log.calledWith(`Text translated to ${lang}`)); + }); }); -it('translateText translates and publishes text with Node 8 arguments', async () => { - const data = { - data: Buffer.from( - JSON.stringify({ - text, - filename, - lang, - }) - ).toString('base64'), - }; - const sample = getSample(); - - sample.mocks.translate.translate.returns(Promise.resolve([translation])); - - await sample.program.translateText(data); - assert.strictEqual(console.log.callCount, 2); - assert.deepStrictEqual(console.log.firstCall.args, [ - `Translating text into ${lang}`, - ]); - assert.deepStrictEqual(console.log.secondCall.args, [ - `Text translated to ${lang}`, - ]); -}); - -it('saveResult fails without text', async () => { - const error = new Error( - 'Text not provided. Make sure you have a "text" property in your request' - ); - const event = { - data: { - data: Buffer.from(JSON.stringify({})).toString('base64'), - }, - }; - - try { - await getSample().program.saveResult(event); - } catch (err) { - assert.deepStrictEqual(err, error); - } -}); - -it('saveResult fails without a filename', async () => { - const error = new Error( - 'Filename not provided. Make sure you have a "filename" property in your request' - ); - const event = { - data: { - data: Buffer.from(JSON.stringify({text})).toString('base64'), - }, - }; - - try { - await getSample().program.saveResult(event); - } catch (err) { - assert.deepStrictEqual(err, error); - } -}); - -it('saveResult fails without a lang', async () => { - const error = new Error( - 'Language not provided. Make sure you have a "lang" property in your request' - ); - const event = { - data: { +describe('saveResult', () => { + it('saveResult validates parameters', async () => { + const event = { data: Buffer.from(JSON.stringify({text, filename})).toString('base64'), - }, - }; - - try { - await getSample().program.saveResult(event); - } catch (err) { - assert.deepStrictEqual(err, error); - } -}); - -it('saveResult translates and publishes text with Node 6 arguments', async () => { - const event = { - data: { + }; + + try { + await program.saveResult(event); + assert.fail('no error thrown'); + } catch (err) { + assert.deepStrictEqual(err.message, errorMsg('Language', 'lang')); + } + }); + + it('saveResult translates and publishes text', async () => { + const data = { data: Buffer.from(JSON.stringify({text, filename, lang})).toString( 'base64' ), - }, - }; - const sample = getSample(); - - await sample.program.saveResult(event); - assert.strictEqual(console.log.callCount, 3); - assert.deepStrictEqual(console.log.getCall(0).args, [ - `Received request to save file ${filename}`, - ]); - assert.deepStrictEqual(console.log.getCall(1).args, [ - `Saving result to ${filename}_to_${lang}.txt in bucket ${sample.mocks.config.RESULT_BUCKET}`, - ]); - assert.deepStrictEqual(console.log.getCall(2).args, ['File saved.']); -}); - -it('saveResult translates and publishes text with Node 8 arguments', async () => { - const data = { - data: Buffer.from(JSON.stringify({text, filename, lang})).toString( - 'base64' - ), - }; - const sample = getSample(); - - await sample.program.saveResult(data); - assert.strictEqual(console.log.callCount, 3); - assert.deepStrictEqual(console.log.getCall(0).args, [ - `Received request to save file ${filename}`, - ]); - assert.deepStrictEqual(console.log.getCall(1).args, [ - `Saving result to ${filename}_to_${lang}.txt in bucket ${sample.mocks.config.RESULT_BUCKET}`, - ]); - assert.deepStrictEqual(console.log.getCall(2).args, ['File saved.']); -}); + }; -it('saveResult translates and publishes text with dot in filename', async () => { - const event = { - data: { - data: Buffer.from( - JSON.stringify({text, filename: `${filename}.jpg`, lang}) - ).toString('base64'), - }, - }; - const sample = getSample(); + const newFilename = `${filename}_to_${lang}.txt`; - await sample.program.saveResult(event); - assert.strictEqual(console.log.callCount, 3); - assert.deepStrictEqual(console.log.getCall(0).args, [ - `Received request to save file ${filename}.jpg`, - ]); - assert.deepStrictEqual(console.log.getCall(1).args, [ - `Saving result to ${filename}.jpg_to_${lang}.txt in bucket ${sample.mocks.config.RESULT_BUCKET}`, - ]); - assert.deepStrictEqual(console.log.getCall(2).args, ['File saved.']); + await program.saveResult(data); + assert.ok( + console.log.calledWith(`Received request to save file ${filename}`) + ); + assert.ok( + console.log.calledWith( + `Saving result to ${newFilename} in bucket ${RESULT_BUCKET}` + ) + ); + assert.ok(console.log.calledWith('File saved.')); + + // Check file was actually saved + assert.ok( + storage + .bucket(RESULT_BUCKET) + .file(newFilename) + .exists() + ); + }); });