Skip to content

Instantly share code, notes, and snippets.

@claudiosdc
Last active January 27, 2020 10:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save claudiosdc/c2ba3a3bf0c8585afc8ce4e546efbf94 to your computer and use it in GitHub Desktop.
Save claudiosdc/c2ba3a3bf0c8585afc8ce4e546efbf94 to your computer and use it in GitHub Desktop.
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.

function FileRetrievalController(node, context, flow, global, msg) {
const dataChunkSize = 10485760; // (10 MB) Size of binary data before any encoding. The actual number of bytes
// received depends on the encoding used. For base64, it will be 13,981,014
function initRetrieval() {
// New file retrieval being initiated. Reset context
context.set('proc_msgid', msg._msgid);
context.set('msgChunker', undefined);
context.set('messageId', undefined);
context.set('fileInfo', undefined);
// Get ID of Catenis message to retrieve
const messageId = msg.req.params.msgid;
if (messageId) {
const ctnFile = global.get('ctnFile');
// Instantiate MessageChunker object to accumulate file chunks and save it
context.set('msgChunker', new ctnFile.MessageChunker('base64'));
// Save message ID
context.set('messageId', messageId);
// Pass command to retrieve first message chunk via output #2
msg.payload = {
messageId: messageId,
options: {
dataChunkSize: dataChunkSize
}
};
return [null, msg];
}
else {
// No message ID. Report error
node.error('No message ID', msg);
}
}
function contRetrieval() {
// Make sure that this corresponds to the current flow being processed
if (msg._msgid === context.get('proc_msgid')) {
const msgChunker = context.get('msgChunker');
let msgChunk;
if (msgChunker.getBytesCount() === 0) {
// First message chunk. Extract file header
const fileInfo = global.get('ctnFile').FileHeader.decode(new Buffer(msg.payload.msgData, 'base64'));
if (fileInfo) {
// Save file info
context.set('fileInfo', fileInfo);
// And adjust message chunk
msgChunk = fileInfo.fileContents.toString('base64');
}
else {
// Message has no valid file header. Report error
node.error('No valid file header found', msg);
}
}
else {
msgChunk = msg.payload.msgData;
}
// Accumulate message chunks
msgChunker.newMessageChunk(msgChunk);
if (msg.payload.continuationToken) {
// Pass command to retrieve next message chunk via output #2
msg.payload = {
messageId: context.get('messageId'),
options: {
continuationToken: msg.payload.continuationToken
}
};
return [null, msg];
}
else {
// The whole message has been retrieved. Prepare to return file
msg.statusCode = 200;
msg.headers = {
'content-type': context.get('fileInfo').fileType,
'content-disposition': 'attachment; filename*=UTF-8\'\'' + encodeURIComponent(context.get('fileInfo').fileName)
};
msg.payload = Buffer.from(msgChunker.getMessage(), 'base64');
return msg;
}
}
else {
// Wrong flow; nothing to do
node.debug('Received message from Log Message node for a different flow. Current flow (_msgid): ' +
context.get('proc_msgid') + '; received flow (_msgid): ' + msg._msgid);
}
}
let result;
switch (msg.origin) {
case 'get request':
result = initRetrieval();
break;
case 'read message':
result = contRetrieval();
break;
}
if (result) {
return result;
}
}
function FileStorageController(node, context, flow, global, msg) {
const maxChunkSize = 15727616; // (15 MB - 1 KB) Size after base64 encoding
function initStorage() {
// New file storage being initiated. Reset context
context.set('proc_msgid', msg._msgid);
context.set('msgChunker', undefined);
// Get file
const file = msg.req.files[0];
if (file) {
const ctnFile = global.get('ctnFile');
// Prepend file metadata header to file contents
const modifiedFileContents = ctnFile.FileHeader.encode({
fileName: file.originalname,
fileType: file.mimetype,
fileContents: file.buffer
});
// Instantiate MessageChunker object to break up file in chunks
const msgChunker = new ctnFile.MessageChunker(modifiedFileContents, maxChunkSize);
// Get first chunk and prepare to send it to be stored to the blockchain
msg.payload = {
message: {
data: msgChunker.nextMessageChunk(),
isFinal: false
}
};
// Save MessageChunker object
context.set('msgChunker', msgChunker);
// Send message chunk to be stored via output #2
return [null, msg];
}
else {
// No file. Report error
node.error('No file to upload', msg);
}
}
function contStorage() {
// Make sure that this corresponds to the current flow being processed
if (msg._msgid === context.get('proc_msgid')) {
if (msg.payload.continuationToken) {
// Get next message chunk
const msgChunk = context.get('msgChunker').nextMessageChunk();
let message;
if (msgChunk) {
message = {
data: msgChunk,
isFinal: false,
continuationToken: msg.payload.continuationToken
};
}
else {
message = {
isFinal: true,
continuationToken: msg.payload.continuationToken
};
}
// Send next message chunk to be stored via output #2
msg.payload = {
message: message
};
return [null, msg];
}
else {
// Pass ID of resulting Catenis message via output #1
return [msg];
}
}
else {
// Wrong flow; nothing to do
node.debug('Received message from Log Message node for a different flow. Current flow (_msgid): ' +
context.get('proc_msgid') + '; received flow (_msgid): ' + msg._msgid);
}
}
let result;
switch (msg.origin) {
case 'post request':
result = initStorage();
break;
case 'log message':
result = contStorage();
break;
}
if (result) {
return result;
}
}
[{"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"]]}]
function ParseErrorMessage(msg) {
let errMsg = msg.error.message;
const regEx = /^.*\[([0-9]{3})\]\s-\s.+$/;
const match = regEx.exec(errMsg);
msg.statusCode = match ? parseInt(match[1]) : 400;
msg.payload = '<h1>Error processing request</h1>\n';
msg.payload += '<p style="color:red">' + errMsg + '</p>';
return msg;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment