Blockchain file upload/download

Sample flow showing how to upload/download file to/from the Bitcoin blockchain using Catenis Flow nodes.

Requirements

The catenis-file Node.js module must be made available to the Node-RED environment. To do so, install the catenis-file module in the same directory as the Node-RED configuration settings file, which is typically found in Node-RED's default user directory: ~/.node-red/settings.js.

Then edit the settings.js file and add the following property to the functionGlobalContext object: ctnFile: require('catenis-file').

For information about installing the catenis-file module, please refer to catenis-file on GitHub.

Catenis Flow, version 2.1.0, must also be installed on your Node-RED environment.

For information about Catenis Flow and how to install it, please refer to catenis-flow on Node-RED's flows repository.

Usage

After importing the flow, double-click on the "Store file" Catenis Flow log message node to configure it, and add a new Catenis device. Use the Catenis device ID and API access secret provided by Blockchain of Things.

Then double-click on the "Recover file" Catenis Flow read message node to configure it, and select the same Catenis device that had been added for the "Store file" node.

File upload

To upload a file to the Bitcoin blockchain, point your web browser at http://localhost:1880/bcfileupload, select a file and click on Upload file.

If everything goes well, you will get a message ID in response. Take note of the message ID, since you will need it to download the file.

File download

To download a file from the Bitcoin blockchain that had been previously uploaded, point your web browser at http://localhost:1880/bcfiledownload/:messageID, replacing :messageID for the actual message ID that have been returned when the file was uploaded.

If everything goes well, the file will be saved to your local computer.

[{"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"]]}]

Flow Info

Created 3 years, 6 months ago
Updated 1 year, 9 months ago
Rating: not yet rated

Owner

Node Types

Core
  • catch (x1)
  • change (x4)
  • function (x3)
  • http in (x3)
  • http response (x4)
  • tab (x1)
  • template (x2)
Other

Tags

  • Catenis
  • Flow
  • Blockchain
  • of
  • Things
  • file
  • upload
  • download
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option