diff options
Diffstat (limited to 'models/export.js')
-rw-r--r-- | models/export.js | 240 |
1 files changed, 193 insertions, 47 deletions
diff --git a/models/export.js b/models/export.js index c3783679..35e55804 100644 --- a/models/export.js +++ b/models/export.js @@ -1,4 +1,3 @@ -import { Exporter } from './exporter'; /* global JsonRoutes */ if (Meteor.isServer) { // todo XXX once we have a real API in place, move that route there @@ -8,10 +7,10 @@ if (Meteor.isServer) { // on the client instead of copy/pasting the route path manually between the // client and the server. /** - * @operation exportJson + * @operation export * @tag Boards * - * @summary This route is used to export the board to a json file format. + * @summary This route is used to export the board. * * @description If user is already logged-in, pass loginToken as param * "authToken": '/api/boards/:boardId/export?authToken=:token' @@ -47,52 +46,199 @@ if (Meteor.isServer) { JsonRoutes.sendResult(res, 403); } }); +} - /** - * @operation exportCSV/TSV - * @tag Boards - * - * @summary This route is used to export the board to a CSV or TSV file format. - * - * @description If user is already logged-in, pass loginToken as param - * - * See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/ - * for detailed explanations - * - * @param {string} boardId the ID of the board we are exporting - * @param {string} authToken the loginToken - * @param {string} delimiter delimiter to use while building export. Default is comma ',' - */ - Picker.route('/api/boards/:boardId/export/csv', function(params, req, res) { - const boardId = params.boardId; - let user = null; - const loginToken = params.query.authToken; - if (loginToken) { - const hashToken = Accounts._hashLoginToken(loginToken); - user = Meteor.users.findOne({ - 'services.resume.loginTokens.hashedToken': hashToken, +// exporter maybe is broken since Gridfs introduced, add fs and path + +export class Exporter { + constructor(boardId) { + this._boardId = boardId; + } + + build() { + const fs = Npm.require('fs'); + const os = Npm.require('os'); + const path = Npm.require('path'); + + const byBoard = { boardId: this._boardId }; + const byBoardNoLinked = { + boardId: this._boardId, + linkedId: { $in: ['', null] }, + }; + // we do not want to retrieve boardId in related elements + const noBoardId = { + fields: { + boardId: 0, + }, + }; + const result = { + _format: 'wekan-board-1.0.0', + }; + _.extend( + result, + Boards.findOne(this._boardId, { + fields: { + stars: 0, + }, + }), + ); + result.lists = Lists.find(byBoard, noBoardId).fetch(); + result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch(); + result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch(); + result.customFields = CustomFields.find( + { boardIds: { $in: [this.boardId] } }, + { fields: { boardId: 0 } }, + ).fetch(); + result.comments = CardComments.find(byBoard, noBoardId).fetch(); + result.activities = Activities.find(byBoard, noBoardId).fetch(); + result.rules = Rules.find(byBoard, noBoardId).fetch(); + result.checklists = []; + result.checklistItems = []; + result.subtaskItems = []; + result.triggers = []; + result.actions = []; + result.cards.forEach(card => { + result.checklists.push( + ...Checklists.find({ + cardId: card._id, + }).fetch(), + ); + result.checklistItems.push( + ...ChecklistItems.find({ + cardId: card._id, + }).fetch(), + ); + result.subtaskItems.push( + ...Cards.find({ + parentId: card._id, + }).fetch(), + ); + }); + result.rules.forEach(rule => { + result.triggers.push( + ...Triggers.find( + { + _id: rule.triggerId, + }, + noBoardId, + ).fetch(), + ); + result.actions.push( + ...Actions.find( + { + _id: rule.actionId, + }, + noBoardId, + ).fetch(), + ); + }); + + // [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]); }); - } else if (!Meteor.settings.public.sandstorm) { - Authentication.checkUserId(req.userId); - user = Users.findOne({ - _id: req.userId, - isAdmin: true, + + readStream.on('error', function(err) { + callback(null, null); }); - } - const exporter = new Exporter(boardId); - if (exporter.canExport(user)) { - body = params.query.delimiter - ? exporter.buildCsv(params.query.delimiter) - : exporter.buildCsv(); - res.writeHead(200, { - 'Content-Length': body[0].length, - 'Content-Type': params.query.delimiter ? 'text/csv' : 'text/tsv', + readStream.on('end', function() { + // done + fs.unlink(tmpFile, () => { + //ignored + }); + + callback(null, buffer.toString('base64')); }); - res.write(body[0]); - res.end(); - } else { - res.writeHead(403); - res.end('Permission Error'); - } - }); + readStream.pipe(tmpWriteable); + }; + const getBase64DataSync = Meteor.wrapAsync(getBase64Data); + result.attachments = Attachments.find({ 'meta.boardId': byBoard.boardId }) + .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 + // 2- do not export any sensitive information + const users = {}; + result.members.forEach(member => { + users[member.userId] = true; + }); + result.lists.forEach(list => { + users[list.userId] = true; + }); + result.cards.forEach(card => { + users[card.userId] = true; + if (card.members) { + card.members.forEach(memberId => { + users[memberId] = true; + }); + } + }); + result.comments.forEach(comment => { + users[comment.userId] = true; + }); + result.activities.forEach(activity => { + users[activity.userId] = true; + }); + result.checklists.forEach(checklist => { + users[checklist.userId] = true; + }); + const byUserIds = { + _id: { + $in: Object.getOwnPropertyNames(users), + }, + }; + // we use whitelist to be sure we do not expose inadvertently + // some secret fields that gets added to User later. + const userFields = { + fields: { + _id: 1, + username: 1, + 'profile.fullname': 1, + 'profile.initials': 1, + 'profile.avatarUrl': 1, + }, + }; + result.users = Users.find(byUserIds, userFields) + .fetch() + .map(user => { + // user avatar is stored as a relative url, we export absolute + if ((user.profile || {}).avatarUrl) { + user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl); + } + return user; + }); + return result; + } + + canExport(user) { + const board = Boards.findOne(this._boardId); + return board && board.isVisibleBy(user); + } } |