|
[{"id":"33586ce0.e1bbdc","type":"tab","label":"Blockchain file upload/download","disabled":false,"info":"Sample flow showing how to upload/download a file to/from the Bitcoin blockchain using Catenis Flow nodes."},{"id":"e8e491ab.1bddc8","type":"http in","z":"33586ce0.e1bbdc","name":"Upload endpoint","url":"/bcfileupload","method":"get","upload":false,"swaggerDoc":"","x":140,"y":200,"wires":[["9975bd2b.2157c8"]]},{"id":"9975bd2b.2157c8","type":"template","z":"33586ce0.e1bbdc","name":"File upload page","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<h1>Select file to upload:</h1>\n\n<form action=\"/bcfileupload\" method=\"POST\" enctype=\"multipart/form-data\">\n <input type=\"file\" name=\"inFile\" />\n <input type=\"submit\" value=\"Upload File\">\n</form>","output":"str","x":410,"y":200,"wires":[["fc403b80.98494"]]},{"id":"fc403b80.98494","type":"http response","z":"33586ce0.e1bbdc","name":"Return upload page","statusCode":"","headers":{},"x":680,"y":200,"wires":[]},{"id":"edf7f7a4.a9e6f","type":"http in","z":"33586ce0.e1bbdc","name":"Upload backend","url":"/bcfileupload","method":"post","upload":true,"swaggerDoc":"","x":140,"y":260,"wires":[["56149978.222288"]]},{"id":"34a3ac88.0bd2d4","type":"function","z":"33586ce0.e1bbdc","name":"File storage controller","func":"//const maxChunkSize = 15727616; // (15 MB - 1 KB) Size after base64 encoding\nconst maxChunkSize = 10485760;\n\nfunction initStorage() {\n // New file storage being initiated. Reset context\n context.set('proc_msgid', msg._msgid);\n context.set('msgChunker', undefined);\n\n // Get file\n const file = msg.req.files[0];\n\n if (file) {\n const ctnFile = global.get('ctnFile');\n\n // Prepend file metadata header to file contents\n const modifiedFileContents = ctnFile.FileHeader.encode({\n fileName: file.originalname,\n fileType: file.mimetype,\n fileContents: file.buffer\n });\n\n // Instantiate MessageChunker object to break up file in chunks\n const msgChunker = new ctnFile.MessageChunker(modifiedFileContents, maxChunkSize);\n\n // Get first chunk and prepare to send it to be stored to the blockchain\n msg.payload = {\n message: {\n data: msgChunker.nextMessageChunk(),\n isFinal: false\n }\n };\n\n // Save MessageChunker object\n context.set('msgChunker', msgChunker);\n\n // Send message chunk to be stored via output #2\n return [null, msg];\n }\n else {\n // No file. Report error\n node.error('No file to upload', msg);\n }\n}\n\nfunction contStorage() {\n // Make sure that this corresponds to the current flow being processed\n if (msg._msgid === context.get('proc_msgid')) {\n if (msg.payload.continuationToken) {\n // Get next message chunk\n const msgChunk = context.get('msgChunker').nextMessageChunk();\n let message;\n\n if (msgChunk) {\n message = {\n data: msgChunk,\n isFinal: false,\n continuationToken: msg.payload.continuationToken\n };\n }\n else {\n message = {\n isFinal: true,\n continuationToken: msg.payload.continuationToken\n };\n }\n\n // Send next message chunk to be stored via output #2\n msg.payload = {\n message: message\n };\n\n return [null, msg];\n }\n else {\n // Pass ID of resulting Catenis message via output #1\n return [msg];\n }\n }\n else {\n // Wrong flow; nothing to do\n node.debug('Received message from Log Message node for a different flow. Current flow (_msgid): ' +\n context.get('proc_msgid') + '; received flow (_msgid): ' + msg._msgid);\n }\n}\n\nlet result;\n\nswitch (msg.origin) {\n case 'post request':\n result = initStorage();\n break;\n\n case 'log message':\n result = contStorage();\n break;\n}\n\nif (result) {\n return result;\n}","outputs":2,"noerr":0,"x":520,"y":260,"wires":[["fc9c8e43.dc96"],["7d22200c.235b58"]]},{"id":"7d22200c.235b58","type":"log message","z":"33586ce0.e1bbdc","name":"Store file","device":"","encoding":"base64","encrypt":true,"offChain":true,"storage":"external","async":false,"x":160,"y":320,"wires":[["f644c2f2.690288"]]},{"id":"fc9c8e43.dc96","type":"template","z":"33586ce0.e1bbdc","name":"Upload result page","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<h1>File upload result</h1>\n\n<p>File successfully uploaded to the blockchain. Returned message id: {{payload.messageId}}</p>","output":"str","x":750,"y":260,"wires":[["8fc6447c.b84068"]]},{"id":"8fc6447c.b84068","type":"http response","z":"33586ce0.e1bbdc","name":"Return result page","statusCode":"","headers":{},"x":950,"y":260,"wires":[]},{"id":"76197d63.ba4f84","type":"http in","z":"33586ce0.e1bbdc","name":"Download endpoint","url":"/bcfiledownload/:msgid","method":"get","upload":false,"swaggerDoc":"","x":150,"y":420,"wires":[["b9c15ab3.d8833"]]},{"id":"268034d5.7f98c4","type":"read message","z":"33586ce0.e1bbdc","name":"Recover file","device":"","encoding":"base64","dataChunkSize":"10485760","async":false,"x":170,"y":480,"wires":[["d06da2c8.4c461"]]},{"id":"41a734dc.4ca30c","type":"function","z":"33586ce0.e1bbdc","name":"File retrieval controller","func":"//const dataChunkSize = 10485760; // (10 MB) Size of binary data before any encoding. The actual number of bytes\n // received depends on the encoding used. For base64, it will be 13,981,014\nconst dataChunkSize = 7860320;\n\nfunction initRetrieval() {\n // New file retrieval being initiated. Reset context\n context.set('proc_msgid', msg._msgid);\n context.set('msgChunker', undefined);\n context.set('messageId', undefined);\n context.set('fileInfo', undefined);\n\n // Get ID of Catenis message to retrieve\n const messageId = msg.req.params.msgid;\n\n if (messageId) {\n const ctnFile = global.get('ctnFile');\n\n // Instantiate MessageChunker object to accumulate file chunks and save it\n context.set('msgChunker', new ctnFile.MessageChunker('base64'));\n\n // Save message ID\n context.set('messageId', messageId);\n\n // Pass command to retrieve first message chunk via output #2\n msg.payload = {\n messageId: messageId,\n options: {\n dataChunkSize: dataChunkSize\n }\n };\n\n return [null, msg];\n }\n else {\n // No message ID. Report error\n node.error('No message ID', msg);\n }\n}\n\nfunction contRetrieval() {\n // Make sure that this corresponds to the current flow being processed\n if (msg._msgid === context.get('proc_msgid')) {\n const msgChunker = context.get('msgChunker');\n let msgChunk;\n\n if (msgChunker.getBytesCount() === 0) {\n // First message chunk. Extract file header\n const fileInfo = global.get('ctnFile').FileHeader.decode(new Buffer(msg.payload.msgData, 'base64'));\n\n if (fileInfo) {\n // Save file info\n context.set('fileInfo', fileInfo);\n\n // And adjust message chunk\n msgChunk = fileInfo.fileContents.toString('base64');\n }\n else {\n // Message has no valid file header. Report error\n node.error('No valid file header found', msg);\n }\n }\n else {\n msgChunk = msg.payload.msgData;\n }\n\n // Accumulate message chunks\n msgChunker.newMessageChunk(msgChunk);\n\n if (msg.payload.continuationToken) {\n // Pass command to retrieve next message chunk via output #2\n msg.payload = {\n messageId: context.get('messageId'),\n options: {\n continuationToken: msg.payload.continuationToken\n }\n };\n\n return [null, msg];\n }\n else {\n // The whole message has been retrieved. Prepare to return file\n msg.statusCode = 200;\n msg.headers = {\n 'content-type': context.get('fileInfo').fileType,\n 'content-disposition': 'attachment; filename*=UTF-8\\'\\'' + encodeURIComponent(context.get('fileInfo').fileName)\n };\n msg.payload = Buffer.from(msgChunker.getMessage(), 'base64');\n\n return msg;\n }\n }\n else {\n // Wrong flow; nothing to do\n node.debug('Received message from Log Message node for a different flow. Current flow (_msgid): ' +\n context.get('proc_msgid') + '; received flow (_msgid): ' + msg._msgid);\n }\n}\n\nlet result;\n\nswitch (msg.origin) {\n case 'get request':\n result = initRetrieval();\n break;\n\n case 'read message':\n result = contRetrieval();\n break;\n}\n\nif (result) {\n return result;\n}","outputs":2,"noerr":0,"x":540,"y":420,"wires":[["1db9aff8.cbdb7"],["268034d5.7f98c4"]]},{"id":"1db9aff8.cbdb7","type":"http response","z":"33586ce0.e1bbdc","name":"Download response","statusCode":"","headers":{},"x":780,"y":420,"wires":[]},{"id":"6981c3b3.ae1684","type":"catch","z":"33586ce0.e1bbdc","name":"","scope":null,"x":220,"y":100,"wires":[["7dd06212.441aec"]]},{"id":"323528c1.59ba4","type":"http response","z":"33586ce0.e1bbdc","name":"Return error page","statusCode":"","headers":{},"x":670,"y":100,"wires":[]},{"id":"7dd06212.441aec","type":"function","z":"33586ce0.e1bbdc","name":"Parse error message","func":"let errMsg = msg.error.message;\nconst regEx = /^.*\\[([0-9]{3})\\]\\s-\\s.+$/;\nconst match = regEx.exec(errMsg);\n\nmsg.statusCode = match ? parseInt(match[1]) : 400;\nmsg.payload = '<h1>Error processing request</h1>\\n';\nmsg.payload += '<p style=\"color:red\">' + errMsg + '</p>';\n\nreturn msg;","outputs":1,"noerr":0,"x":420,"y":100,"wires":[["323528c1.59ba4"]]},{"id":"56149978.222288","type":"change","z":"33586ce0.e1bbdc","name":"Set origin #1","rules":[{"t":"set","p":"origin","pt":"msg","to":"post request","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":310,"y":260,"wires":[["34a3ac88.0bd2d4"]]},{"id":"f644c2f2.690288","type":"change","z":"33586ce0.e1bbdc","name":"Set origin #2","rules":[{"t":"set","p":"origin","pt":"msg","to":"log message","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":310,"y":320,"wires":[["34a3ac88.0bd2d4"]]},{"id":"b9c15ab3.d8833","type":"change","z":"33586ce0.e1bbdc","name":"Set origin #3","rules":[{"t":"set","p":"origin","pt":"msg","to":"get request","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":420,"wires":[["41a734dc.4ca30c"]]},{"id":"d06da2c8.4c461","type":"change","z":"33586ce0.e1bbdc","name":"Set origin #4","rules":[{"t":"set","p":"origin","pt":"msg","to":"read message","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":480,"wires":[["41a734dc.4ca30c"]]}] |