mirror of
https://github.com/wekan/wekan.git
synced 2025-12-17 07:50:12 +01:00
Included a new route to export (json) an attachment from a board.
GET /api/boards/:id/attachments/:attachmentId/export
This commit is contained in:
parent
a2f8e54b67
commit
6eb90238b1
2 changed files with 109 additions and 49 deletions
|
|
@ -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
|
* @operation exportCSV/TSV
|
||||||
* @tag Boards
|
* @tag Boards
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@ const Papa = require('papaparse');
|
||||||
|
|
||||||
// exporter maybe is broken since Gridfs introduced, add fs and path
|
// exporter maybe is broken since Gridfs introduced, add fs and path
|
||||||
export class Exporter {
|
export class Exporter {
|
||||||
constructor(boardId) {
|
constructor(boardId, attachmentId) {
|
||||||
this._boardId = boardId;
|
this._boardId = boardId;
|
||||||
|
this._attachmentId = attachmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
build() {
|
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.lists = Lists.find(byBoard, noBoardId).fetch();
|
||||||
result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();
|
result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();
|
||||||
result.swimlanes = Swimlanes.find(byBoard, 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
|
// we also have to export some user data - as the other elements only
|
||||||
// include id but we have to be careful:
|
// include id but we have to be careful:
|
||||||
// 1- only exports users that are linked somehow to that board
|
// 1- only exports users that are linked somehow to that board
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue