I did write a firebase function which manipulates a PDF File on request, does some manipulations to it, saves it back to storage and archives a hash to database.
I did manage to get it to work, but its a total mess since I never really learned how to work with pre ES6 js callbacks. I'm new to all of this and learned to work with arrow functions and promises. But here I need to use packages which are pure JavaScript and its working somehow, but I really need to clean up this sync/async mess to do clean error handling and return a promise to firebase after processing the function.
My function also includes some odd file handlings to get the different files ready for the pdf library: For example i create a QR Code, save it to a tmp file, use another library to create a jpg out of the png and save it again to tmp. I am open to any suggestions or hints if I could do something smarter.
Something is wrong with the chain, because the function is completing in a few ms while it still running.
I did add some comments to my code where I have no clue how to change it to ES6 and I would be very grateful if you could help me clean up this big mess.
const hummus = require('hummus');
const request = require('request');
const fs = require('fs');
const sha1 = require('sha1');
const images = require("images");
exports.handleDocSignRequests = functions.database.ref('/user_writeable/docrequests/{uid}').onWrite(event => {
var userReq = event.data.val();
var userRef = event.data.ref;
if (!userReq) return Promise.resolve();
if (!userReq.docpath) return Promise.resolve();
let uid = event.params.uid;
let filename = userReq.docpath; // File to sign and hash
return bucket.file(filename).getSignedUrl({ // getting downloadurl from Firebase Storage
action: 'read'
}).then(
(downloadpath) => {
downloadpath = downloadpath[0];
//download pdf - how to turn this into a promise?
download = function (uri, filename, callback) {
request.head(uri, function (err, res, body) {
request(uri).pipe(fs.createWriteStream(filename)).on('close', callback);
});
};
let pdfsourse = LOCAL_TMP_FOLDER + 'downloadedfile.pdf';
return download(downloadpath, pdfsourse, function () { // download callback turn this into ES6
console.log('download finished');
let qrjpg = LOCAL_TMP_FOLDER + 'qrcode.jpg';
var qrpng = LOCAL_TMP_FOLDER + 'qrcode.png';
let qrurl = 'https://some.url/' + userReq.docid;
let pdfdest = LOCAL_TMP_FOLDER + 'newpdf.pdf';
let logfile = './hummus.log';
QRCode.toFile(qrpng, qrurl, { // how to make this a part of the "chain" and go on when finished
version: 4, type: "png", scale: 2, margin: 0
}, function (err) {
if (err) throw err;
console.log('qr png ready');
images(qrpng).save(qrjpg, {operation: 100}); // save png as jpg
console.log('qr jpg ready');
});
// Doing stuff to PDF with HummusJs
let pdfWriter = hummus.createWriterToModify(pdfsourse, {
modifiedFilePath: pdfdest,
log: logfile,
userPassword: 'user',
ownerPassword: 'owner',
userProtectionFlag: 4
});
let pdfReader = pdfWriter.getModifiedFileParser();
let arialFont = pdfWriter.getFontForFile(ariallocal);
let textOptions = {font: arialFont, size: 5, colorspace: 'gray', color: 0x00};
for (let i = 0; i < pdfReader.getPagesCount(); ++i) {
let pageBox = pdfReader.parsePage(i).getMediaBox();
let pageModifier = new hummus.PDFPageModifier(pdfWriter, i, true);
let ctx = pageModifier.startContext().getContext();
ctx.writeText('Document saved', 5, 110, textOptions);
ctx.drawImage(5, 52, qrfile,
{
transformation: {
width: 40,
height: 40,
fit: 'always'
}
});
pageModifier.endContext().writePage();
}
pdfWriter.end();
// How can I be sure PDF is done and written to tmp file? Or is this given by sync function?
// Reading finished PDF from file again, to get base64 for hashing - is there a better way?
let newpdf = fs.readFileSync(pdfdest);
let base64pdf = newpdf.toString('base64');
let hash = sha1(base64pdf);
let signobj = {};
signobj['hash'] = hash;
// Check if document already in database, if not write hash to database,
// upload finished pdf to original place and archive
// and return remove request
let sign_ref = docsign_ref.child(userReq.docid);
return sign_ref.once('value').then(function (snap) {
if (!snap.val()) { //Document is new
let upload1 = bucket.upload(destcry, {destination: filename}).then
(suc => {
console.log('uploaded');
});
//
let upload2 = bucket.upload(destcry, {destination: 'signed/' + userReq.docid + '.pdf'}).then
(suc => {
console.log('uploaded');
});
return Promise.all([upload1, upload2]).then( // When both uploads are finished go on
(suc) => {
return sign_ref.set(signobj).then(
(suc) => {
// Remove Request and return Promise
return userRef.remove();
});
});
}
else {
//Document already in database, this should never happen, only for seq reasons
console.log('doc already in database);
return Promise.resolve();
}
});
});
});
});
As I wrote in comments, your question is too broad. I will focus here on the question as you put it in the title.
The main improvement when it comes to ES6 promises, is that you should:
then
callsFinally, when you call the outer function, make sure to treat it as a promise: once you have asynchronous stuff going on, you need to stick with that pattern. So you would call your main function like this:
handleDocSignRequests().then( .... );
Here is some untested(!) rewrite of your code to address the above two points. The main changes are commented with ***
. There might be some mistakes, but the pattern should be clear:
const hummus = require('hummus');
const request = require('request');
const fs = require('fs');
const sha1 = require('sha1');
const images = require("images");
exports.handleDocSignRequests = functions.database.ref('/user_writeable/docrequests/{uid}').onWrite(event => {
var userReq = event.data.val();
var userRef = event.data.ref;
if (!userReq) return Promise.resolve();
if (!userReq.docpath) return Promise.resolve();
let uid = event.params.uid;
let filename = userReq.docpath; // File to sign and hash
// *** Define these variables at this level, so they are accessible throughout
// the flattened promise-chain:
let signobj = {};
let sign_ref = docsign_ref.child(userReq.docid);
return bucket.file(filename).getSignedUrl({ // getting downloadurl from Firebase Storage
action: 'read'
}).then((downloadpath) => {
downloadpath = downloadpath[0];
let pdfsourse = LOCAL_TMP_FOLDER + 'downloadedfile.pdf';
// *** turned into promise:
// *** return the promise to the main promise chain
return new Promise(resolve => {
request.head(downloadpath, function (err, res, body) {
request(downloadpath)
.pipe(fs.createWriteStream(pdfsourse))
.on('close', resolve);
});
});
}).then(function () {
console.log('download finished');
let qrjpg = LOCAL_TMP_FOLDER + 'qrcode.jpg';
var qrpng = LOCAL_TMP_FOLDER + 'qrcode.png';
let qrurl = 'https://some.url/' + userReq.docid;
// *** Return a promise to make this a part of the "chain" and go on when finished
return new Promise( (resolve, reject) => {
QRCode.toFile(qrpng, qrurl, {
version: 4, type: "png", scale: 2, margin: 0
}, function (err) {
if (err) reject(err); // *** reject
console.log('qr png ready');
images(qrpng).save(qrjpg, {operation: 100}); // save png as jpg
console.log('qr jpg ready');
resolve(); // *** resolve the promise now it's done
});
});
}).then(function () {
// Doing stuff to PDF with HummusJs
let pdfdest = LOCAL_TMP_FOLDER + 'newpdf.pdf';
let logfile = './hummus.log';
let pdfWriter = hummus.createWriterToModify(pdfsourse, {
modifiedFilePath: pdfdest,
log: logfile,
userPassword: 'user',
ownerPassword: 'owner',
userProtectionFlag: 4
});
let pdfReader = pdfWriter.getModifiedFileParser();
let arialFont = pdfWriter.getFontForFile(ariallocal);
let textOptions = {font: arialFont, size: 5, colorspace: 'gray', color: 0x00};
for (let i = 0; i < pdfReader.getPagesCount(); ++i) {
let pageBox = pdfReader.parsePage(i).getMediaBox();
let pageModifier = new hummus.PDFPageModifier(pdfWriter, i, true);
let ctx = pageModifier.startContext().getContext();
ctx.writeText('Document saved', 5, 110, textOptions);
ctx.drawImage(5, 52, qrfile, {
transformation: {
width: 40,
height: 40,
fit: 'always'
}
});
pageModifier.endContext().writePage();
}
pdfWriter.end();
// How can I be sure PDF is done and written to tmp file? Or is this given by sync function?
// Reading finished PDF from file again, to get base64 for hashing - is there a better way?
let newpdf = fs.readFileSync(pdfdest);
let base64pdf = newpdf.toString('base64');
let hash = sha1(base64pdf);
signobj['hash'] = hash;
// Check if document already in database, if not write hash to database,
// upload finished pdf to original place and archive
// and return remove request
// *** Only return the basic promise. Perform the `then` on the outer chain.
return sign_ref.once('value');
}).then(function (snap) {
// *** Do simple case first:
if (snap.val()) {
//Document already in database, this should never happen, only for seq reasons
console.log('doc already in database);
return Promise.resolve();
} // *** No need for `else` here. The above `return` is enough
//Document is new
// *** use array to replace two blocks of very similar code
let promises = [filename, 'signed/' + userReq.docid + '.pdf'].map(destination =>
return bucket.upload(destcry, {destination}).then(suc => {
console.log('uploaded');
});
});
// *** Return the basic promise. Perform the `then` on the outer chain
return Promise.all(promises);// Wait for both uploads to finish
}).then( suc => {
// *** I suppose this also has to run when not a new document....
// *** Again: return the promise and perform the `then` on the outer chain
return sign_ref.set(signobj);
}).then( suc => {
// Remove Request and return Promise
return userRef.remove();
});
});