wekan/server/routes/attachmentApi.js

554 lines
18 KiB
JavaScript
Raw Normal View History

import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import { ReactiveCache } from '/imports/reactiveCache';
import { Attachments, fileStoreStrategyFactory } from '/models/attachments';
import { moveToStorage } from '/models/lib/fileStoreStrategy';
import { STORAGE_NAME_FILESYSTEM, STORAGE_NAME_GRIDFS, STORAGE_NAME_S3 } from '/models/lib/fileStoreStrategy';
import AttachmentStorageSettings from '/models/attachmentStorageSettings';
import fs from 'fs';
import path from 'path';
import { ObjectID } from 'bson';
// Attachment API HTTP routes
if (Meteor.isServer) {
// Helper function to authenticate API requests
function authenticateApiRequest(req) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new Meteor.Error('unauthorized', 'Missing or invalid authorization header');
}
const token = authHeader.substring(7);
// Here you would validate the token and get the user ID
// For now, we'll use a simple approach - in production, you'd want proper JWT validation
const userId = token; // This should be replaced with proper token validation
if (!userId) {
throw new Meteor.Error('unauthorized', 'Invalid token');
}
return userId;
}
// Helper function to send JSON response
function sendJsonResponse(res, statusCode, data) {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}
// Helper function to send error response
function sendErrorResponse(res, statusCode, message) {
sendJsonResponse(res, statusCode, { success: false, error: message });
}
// Upload attachment endpoint
WebApp.connectHandlers.use('/api/attachment/upload', (req, res, next) => {
if (req.method !== 'POST') {
return next();
}
try {
const userId = authenticateApiRequest(req);
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const data = JSON.parse(body);
const { boardId, swimlaneId, listId, cardId, fileData, fileName, fileType, storageBackend } = data;
// Validate parameters
if (!boardId || !swimlaneId || !listId || !cardId || !fileData || !fileName) {
return sendErrorResponse(res, 400, 'Missing required parameters');
}
// Check if user has permission to modify the card
const card = ReactiveCache.getCard(cardId);
if (!card) {
return sendErrorResponse(res, 404, 'Card not found');
}
const board = ReactiveCache.getBoard(boardId);
if (!board) {
return sendErrorResponse(res, 404, 'Board not found');
}
// Check permissions
if (!board.isBoardMember(userId)) {
return sendErrorResponse(res, 403, 'You do not have permission to modify this card');
}
// Check if board allows attachments
if (!board.allowsAttachments) {
return sendErrorResponse(res, 403, 'Attachments are not allowed on this board');
}
// Get default storage backend if not specified
let targetStorage = storageBackend;
if (!targetStorage) {
try {
const settings = AttachmentStorageSettings.findOne({});
targetStorage = settings ? settings.getDefaultStorage() : STORAGE_NAME_FILESYSTEM;
} catch (error) {
targetStorage = STORAGE_NAME_FILESYSTEM;
}
}
// Validate storage backend
if (![STORAGE_NAME_FILESYSTEM, STORAGE_NAME_GRIDFS, STORAGE_NAME_S3].includes(targetStorage)) {
return sendErrorResponse(res, 400, 'Invalid storage backend');
}
// Create file object from base64 data
const fileBuffer = Buffer.from(fileData, 'base64');
const file = new File([fileBuffer], fileName, { type: fileType || 'application/octet-stream' });
// Create attachment metadata
const fileId = new ObjectID().toString();
const meta = {
boardId: boardId,
swimlaneId: swimlaneId,
listId: listId,
cardId: cardId,
fileId: fileId,
source: 'api',
storageBackend: targetStorage
};
// Create attachment
const uploader = Attachments.insert({
file: file,
meta: meta,
isBase64: false,
transport: 'http'
});
if (uploader) {
// Move to target storage if not filesystem
if (targetStorage !== STORAGE_NAME_FILESYSTEM) {
Meteor.defer(() => {
try {
moveToStorage(uploader, targetStorage, fileStoreStrategyFactory);
} catch (error) {
console.error('Error moving attachment to target storage:', error);
}
});
}
sendJsonResponse(res, 200, {
success: true,
attachmentId: uploader._id,
fileName: fileName,
fileSize: fileBuffer.length,
storageBackend: targetStorage,
message: 'Attachment uploaded successfully'
});
} else {
sendErrorResponse(res, 500, 'Failed to upload attachment');
}
} catch (error) {
console.error('API attachment upload error:', error);
sendErrorResponse(res, 500, error.message);
}
});
} catch (error) {
sendErrorResponse(res, 401, error.message);
}
});
// Download attachment endpoint
WebApp.connectHandlers.use('/api/attachment/download/([^/]+)', (req, res, next) => {
if (req.method !== 'GET') {
return next();
}
try {
const userId = authenticateApiRequest(req);
const attachmentId = req.params[0];
// Get attachment
const attachment = ReactiveCache.getAttachment(attachmentId);
if (!attachment) {
return sendErrorResponse(res, 404, 'Attachment not found');
}
// Check permissions
const board = ReactiveCache.getBoard(attachment.meta.boardId);
if (!board || !board.isBoardMember(userId)) {
return sendErrorResponse(res, 403, 'You do not have permission to access this attachment');
}
// Get file strategy
const strategy = fileStoreStrategyFactory.getFileStrategy(attachment, 'original');
const readStream = strategy.getReadStream();
if (!readStream) {
return sendErrorResponse(res, 404, 'File not found in storage');
}
// Read file data
const chunks = [];
readStream.on('data', (chunk) => {
chunks.push(chunk);
});
readStream.on('end', () => {
const fileBuffer = Buffer.concat(chunks);
const base64Data = fileBuffer.toString('base64');
sendJsonResponse(res, 200, {
success: true,
attachmentId: attachmentId,
fileName: attachment.name,
fileSize: attachment.size,
fileType: attachment.type,
base64Data: base64Data,
storageBackend: strategy.getStorageName()
});
});
readStream.on('error', (error) => {
console.error('Download error:', error);
sendErrorResponse(res, 500, error.message);
});
} catch (error) {
sendErrorResponse(res, 401, error.message);
}
});
// List attachments endpoint
WebApp.connectHandlers.use('/api/attachment/list/([^/]+)/([^/]+)/([^/]+)/([^/]+)', (req, res, next) => {
if (req.method !== 'GET') {
return next();
}
try {
const userId = authenticateApiRequest(req);
const boardId = req.params[0];
const swimlaneId = req.params[1];
const listId = req.params[2];
const cardId = req.params[3];
// Check permissions
const board = ReactiveCache.getBoard(boardId);
if (!board || !board.isBoardMember(userId)) {
return sendErrorResponse(res, 403, 'You do not have permission to access this board');
}
let query = { 'meta.boardId': boardId };
if (swimlaneId && swimlaneId !== 'null') {
query['meta.swimlaneId'] = swimlaneId;
}
if (listId && listId !== 'null') {
query['meta.listId'] = listId;
}
if (cardId && cardId !== 'null') {
query['meta.cardId'] = cardId;
}
const attachments = ReactiveCache.getAttachments(query);
const attachmentList = attachments.map(attachment => {
const strategy = fileStoreStrategyFactory.getFileStrategy(attachment, 'original');
return {
attachmentId: attachment._id,
fileName: attachment.name,
fileSize: attachment.size,
fileType: attachment.type,
storageBackend: strategy.getStorageName(),
boardId: attachment.meta.boardId,
swimlaneId: attachment.meta.swimlaneId,
listId: attachment.meta.listId,
cardId: attachment.meta.cardId,
createdAt: attachment.uploadedAt,
isImage: attachment.isImage
};
});
sendJsonResponse(res, 200, {
success: true,
attachments: attachmentList,
count: attachmentList.length
});
} catch (error) {
sendErrorResponse(res, 401, error.message);
}
});
// Copy attachment endpoint
WebApp.connectHandlers.use('/api/attachment/copy', (req, res, next) => {
if (req.method !== 'POST') {
return next();
}
try {
const userId = authenticateApiRequest(req);
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const data = JSON.parse(body);
const { attachmentId, targetBoardId, targetSwimlaneId, targetListId, targetCardId } = data;
// Get source attachment
const sourceAttachment = ReactiveCache.getAttachment(attachmentId);
if (!sourceAttachment) {
return sendErrorResponse(res, 404, 'Source attachment not found');
}
// Check source permissions
const sourceBoard = ReactiveCache.getBoard(sourceAttachment.meta.boardId);
if (!sourceBoard || !sourceBoard.isBoardMember(userId)) {
return sendErrorResponse(res, 403, 'You do not have permission to access the source attachment');
}
// Check target permissions
const targetBoard = ReactiveCache.getBoard(targetBoardId);
if (!targetBoard || !targetBoard.isBoardMember(userId)) {
return sendErrorResponse(res, 403, 'You do not have permission to modify the target card');
}
// Check if target board allows attachments
if (!targetBoard.allowsAttachments) {
return sendErrorResponse(res, 403, 'Attachments are not allowed on the target board');
}
// Get source file strategy
const sourceStrategy = fileStoreStrategyFactory.getFileStrategy(sourceAttachment, 'original');
const readStream = sourceStrategy.getReadStream();
if (!readStream) {
return sendErrorResponse(res, 404, 'Source file not found in storage');
}
// Read source file data
const chunks = [];
readStream.on('data', (chunk) => {
chunks.push(chunk);
});
readStream.on('end', () => {
try {
const fileBuffer = Buffer.concat(chunks);
const file = new File([fileBuffer], sourceAttachment.name, { type: sourceAttachment.type });
// Create new attachment metadata
const fileId = new ObjectID().toString();
const meta = {
boardId: targetBoardId,
swimlaneId: targetSwimlaneId,
listId: targetListId,
cardId: targetCardId,
fileId: fileId,
source: 'api-copy',
copyFrom: attachmentId,
copyStorage: sourceStrategy.getStorageName()
};
// Create new attachment
const uploader = Attachments.insert({
file: file,
meta: meta,
isBase64: false,
transport: 'http'
});
if (uploader) {
sendJsonResponse(res, 200, {
success: true,
sourceAttachmentId: attachmentId,
newAttachmentId: uploader._id,
fileName: sourceAttachment.name,
fileSize: sourceAttachment.size,
message: 'Attachment copied successfully'
});
} else {
sendErrorResponse(res, 500, 'Failed to copy attachment');
}
} catch (error) {
sendErrorResponse(res, 500, error.message);
}
});
readStream.on('error', (error) => {
sendErrorResponse(res, 500, error.message);
});
} catch (error) {
console.error('API attachment copy error:', error);
sendErrorResponse(res, 500, error.message);
}
});
} catch (error) {
sendErrorResponse(res, 401, error.message);
}
});
// Move attachment endpoint
WebApp.connectHandlers.use('/api/attachment/move', (req, res, next) => {
if (req.method !== 'POST') {
return next();
}
try {
const userId = authenticateApiRequest(req);
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const data = JSON.parse(body);
const { attachmentId, targetBoardId, targetSwimlaneId, targetListId, targetCardId } = data;
// Get source attachment
const sourceAttachment = ReactiveCache.getAttachment(attachmentId);
if (!sourceAttachment) {
return sendErrorResponse(res, 404, 'Source attachment not found');
}
// Check source permissions
const sourceBoard = ReactiveCache.getBoard(sourceAttachment.meta.boardId);
if (!sourceBoard || !sourceBoard.isBoardMember(userId)) {
return sendErrorResponse(res, 403, 'You do not have permission to access the source attachment');
}
// Check target permissions
const targetBoard = ReactiveCache.getBoard(targetBoardId);
if (!targetBoard || !targetBoard.isBoardMember(userId)) {
return sendErrorResponse(res, 403, 'You do not have permission to modify the target card');
}
// Check if target board allows attachments
if (!targetBoard.allowsAttachments) {
return sendErrorResponse(res, 403, 'Attachments are not allowed on the target board');
}
// Update attachment metadata
Attachments.update(attachmentId, {
$set: {
'meta.boardId': targetBoardId,
'meta.swimlaneId': targetSwimlaneId,
'meta.listId': targetListId,
'meta.cardId': targetCardId,
'meta.source': 'api-move',
'meta.movedAt': new Date()
}
});
sendJsonResponse(res, 200, {
success: true,
attachmentId: attachmentId,
fileName: sourceAttachment.name,
fileSize: sourceAttachment.size,
sourceBoardId: sourceAttachment.meta.boardId,
targetBoardId: targetBoardId,
message: 'Attachment moved successfully'
});
} catch (error) {
console.error('API attachment move error:', error);
sendErrorResponse(res, 500, error.message);
}
});
} catch (error) {
sendErrorResponse(res, 401, error.message);
}
});
// Delete attachment endpoint
WebApp.connectHandlers.use('/api/attachment/delete/([^/]+)', (req, res, next) => {
if (req.method !== 'DELETE') {
return next();
}
try {
const userId = authenticateApiRequest(req);
const attachmentId = req.params[0];
// Get attachment
const attachment = ReactiveCache.getAttachment(attachmentId);
if (!attachment) {
return sendErrorResponse(res, 404, 'Attachment not found');
}
// Check permissions
const board = ReactiveCache.getBoard(attachment.meta.boardId);
if (!board || !board.isBoardMember(userId)) {
return sendErrorResponse(res, 403, 'You do not have permission to delete this attachment');
}
// Delete attachment
Attachments.remove(attachmentId);
sendJsonResponse(res, 200, {
success: true,
attachmentId: attachmentId,
fileName: attachment.name,
message: 'Attachment deleted successfully'
});
} catch (error) {
sendErrorResponse(res, 401, error.message);
}
});
// Get attachment info endpoint
WebApp.connectHandlers.use('/api/attachment/info/([^/]+)', (req, res, next) => {
if (req.method !== 'GET') {
return next();
}
try {
const userId = authenticateApiRequest(req);
const attachmentId = req.params[0];
// Get attachment
const attachment = ReactiveCache.getAttachment(attachmentId);
if (!attachment) {
return sendErrorResponse(res, 404, 'Attachment not found');
}
// Check permissions
const board = ReactiveCache.getBoard(attachment.meta.boardId);
if (!board || !board.isBoardMember(userId)) {
return sendErrorResponse(res, 403, 'You do not have permission to access this attachment');
}
const strategy = fileStoreStrategyFactory.getFileStrategy(attachment, 'original');
sendJsonResponse(res, 200, {
success: true,
attachmentId: attachment._id,
fileName: attachment.name,
fileSize: attachment.size,
fileType: attachment.type,
storageBackend: strategy.getStorageName(),
boardId: attachment.meta.boardId,
swimlaneId: attachment.meta.swimlaneId,
listId: attachment.meta.listId,
cardId: attachment.meta.cardId,
createdAt: attachment.uploadedAt,
isImage: attachment.isImage,
versions: Object.keys(attachment.versions).map(versionName => ({
versionName: versionName,
storage: attachment.versions[versionName].storage,
size: attachment.versions[versionName].size,
type: attachment.versions[versionName].type
}))
});
} catch (error) {
sendErrorResponse(res, 401, error.message);
}
});
}