From 6eb90238b1b1dec4f37a3727ed29ed82d2a43cc6 Mon Sep 17 00:00:00 2001 From: Vagner Nascimento Date: Fri, 26 Mar 2021 22:37:42 -0300 Subject: [PATCH] Included a new route to export (json) an attachment from a board. GET /api/boards/:id/attachments/:attachmentId/export --- models/export.js | 51 +++++++++++++++++++++ models/exporter.js | 107 ++++++++++++++++++++++++--------------------- 2 files changed, 109 insertions(+), 49 deletions(-) diff --git a/models/export.js b/models/export.js index 17b08dad8..7b74c20fe 100644 --- a/models/export.js +++ b/models/export.js @@ -48,6 +48,57 @@ if (Meteor.isServer) { } }); + // todo XXX once we have a real API in place, move that route there + // todo XXX also share the route definition between the client and the server + // so that we could use something like + // `ApiRoutes.path('boards/export', boardId)`` + // on the client instead of copy/pasting the route path manually between the + // client and the server. + /** + * @operation exportJson + * @tag Boards + * + * @summary This route is used to export a attachement to a json file format. + * + * @description If user is already logged-in, pass loginToken as param + * "authToken": '/api/boards/:boardId/attachments/:attachmentId/export?authToken=:token' + * + * + * @param {string} boardId the ID of the board we are exporting + * @param {string} attachmentId the ID of the attachment we are exporting + * @param {string} authToken the loginToken + */ + JsonRoutes.add( + 'get', + '/api/boards/:boardId/attachments/:attachmentId/export', + function(req, res) { + const boardId = req.params.boardId; + const attachmentId = req.params.attachmentId; + let user = null; + const loginToken = req.query.authToken; + if (loginToken) { + const hashToken = Accounts._hashLoginToken(loginToken); + user = Meteor.users.findOne({ + 'services.resume.loginTokens.hashedToken': hashToken, + }); + } else if (!Meteor.settings.public.sandstorm) { + Authentication.checkUserId(req.userId); + user = Users.findOne({ _id: req.userId, isAdmin: true }); + } + const exporter = new Exporter(boardId, attachmentId); + if (exporter.canExport(user)) { + JsonRoutes.sendResult(res, { + code: 200, + data: exporter.build(), + }); + } else { + // we could send an explicit error message, but on the other hand the only + // way to get there is by hacking the UI so let's keep it raw. + JsonRoutes.sendResult(res, 403); + } + }, + ); + /** * @operation exportCSV/TSV * @tag Boards diff --git a/models/exporter.js b/models/exporter.js index 0c6d2a4f2..999f30606 100644 --- a/models/exporter.js +++ b/models/exporter.js @@ -2,8 +2,9 @@ const Papa = require('papaparse'); // exporter maybe is broken since Gridfs introduced, add fs and path export class Exporter { - constructor(boardId) { + constructor(boardId, attachmentId) { this._boardId = boardId; + this._attachmentId = attachmentId; } build() { @@ -33,6 +34,62 @@ export class Exporter { }, }), ); + + // [Old] for attachments we only export IDs and absolute url to original doc + // [New] Encode attachment to base64 + + const getBase64Data = function(doc, callback) { + let buffer = Buffer.allocUnsafe(0); + buffer.fill(0); + + // callback has the form function (err, res) {} + const tmpFile = path.join( + os.tmpdir(), + `tmpexport${process.pid}${Math.random()}`, + ); + const tmpWriteable = fs.createWriteStream(tmpFile); + const readStream = doc.createReadStream(); + readStream.on('data', function(chunk) { + buffer = Buffer.concat([buffer, chunk]); + }); + + readStream.on('error', function() { + callback(null, null); + }); + readStream.on('end', function() { + // done + fs.unlink(tmpFile, () => { + //ignored + }); + + callback(null, buffer.toString('base64')); + }); + readStream.pipe(tmpWriteable); + }; + const getBase64DataSync = Meteor.wrapAsync(getBase64Data); + const byBoardAndAttachment = this._attachmentId + ? { boardId: this._boardId, _id: this._attachmentId } + : byBoard; + result.attachments = Attachments.find(byBoardAndAttachment) + .fetch() + .map(attachment => { + let filebase64 = null; + filebase64 = getBase64DataSync(attachment); + + return { + _id: attachment._id, + cardId: attachment.cardId, + //url: FlowRouter.url(attachment.url()), + file: filebase64, + name: attachment.original.name, + type: attachment.original.type, + }; + }); + //When has a especific valid attachment return the single element + if (this._attachmentId) { + return result.attachments.length > 0 ? result.attachments[0] : {}; + } + result.lists = Lists.find(byBoard, noBoardId).fetch(); result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch(); result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch(); @@ -84,54 +141,6 @@ export class Exporter { ); }); - // [Old] for attachments we only export IDs and absolute url to original doc - // [New] Encode attachment to base64 - - const getBase64Data = function(doc, callback) { - let buffer = Buffer.allocUnsafe(0); - buffer.fill(0); - - // callback has the form function (err, res) {} - const tmpFile = path.join( - os.tmpdir(), - `tmpexport${process.pid}${Math.random()}`, - ); - const tmpWriteable = fs.createWriteStream(tmpFile); - const readStream = doc.createReadStream(); - readStream.on('data', function(chunk) { - buffer = Buffer.concat([buffer, chunk]); - }); - - readStream.on('error', function() { - callback(null, null); - }); - readStream.on('end', function() { - // done - fs.unlink(tmpFile, () => { - //ignored - }); - - callback(null, buffer.toString('base64')); - }); - readStream.pipe(tmpWriteable); - }; - const getBase64DataSync = Meteor.wrapAsync(getBase64Data); - result.attachments = Attachments.find(byBoard) - .fetch() - .map(attachment => { - let filebase64 = null; - filebase64 = getBase64DataSync(attachment); - - return { - _id: attachment._id, - cardId: attachment.cardId, - //url: FlowRouter.url(attachment.url()), - file: filebase64, - name: attachment.original.name, - type: attachment.original.type, - }; - }); - // we also have to export some user data - as the other elements only // include id but we have to be careful: // 1- only exports users that are linked somehow to that board