mirror of https://github.com/wekan/wekan
Added attachments API and admin panel attachment management for file storage backends settings. Fixed drag drop upload attachments from file manager to minicard or opened card.
Thanks to xet7 !pull/5953/head
parent
2f95431c9b
commit
ae1f80a52c
@ -0,0 +1,384 @@ |
||||
import { ReactiveCache } from '/imports/reactiveCache'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema'; |
||||
import { STORAGE_NAME_FILESYSTEM, STORAGE_NAME_GRIDFS, STORAGE_NAME_S3 } from '/models/lib/fileStoreStrategy'; |
||||
|
||||
// Attachment Storage Settings Collection
|
||||
AttachmentStorageSettings = new Mongo.Collection('attachmentStorageSettings'); |
||||
|
||||
// Schema for attachment storage settings
|
||||
AttachmentStorageSettings.attachSchema( |
||||
new SimpleSchema({ |
||||
// Default storage backend for new uploads
|
||||
defaultStorage: { |
||||
type: String, |
||||
allowedValues: [STORAGE_NAME_FILESYSTEM, STORAGE_NAME_GRIDFS, STORAGE_NAME_S3], |
||||
defaultValue: STORAGE_NAME_FILESYSTEM, |
||||
label: 'Default Storage Backend' |
||||
}, |
||||
|
||||
// Storage backend configuration
|
||||
storageConfig: { |
||||
type: Object, |
||||
optional: true, |
||||
label: 'Storage Configuration' |
||||
}, |
||||
|
||||
'storageConfig.filesystem': { |
||||
type: Object, |
||||
optional: true, |
||||
label: 'Filesystem Configuration' |
||||
}, |
||||
|
||||
'storageConfig.filesystem.enabled': { |
||||
type: Boolean, |
||||
defaultValue: true, |
||||
label: 'Filesystem Storage Enabled' |
||||
}, |
||||
|
||||
'storageConfig.filesystem.path': { |
||||
type: String, |
||||
optional: true, |
||||
label: 'Filesystem Storage Path' |
||||
}, |
||||
|
||||
'storageConfig.gridfs': { |
||||
type: Object, |
||||
optional: true, |
||||
label: 'GridFS Configuration' |
||||
}, |
||||
|
||||
'storageConfig.gridfs.enabled': { |
||||
type: Boolean, |
||||
defaultValue: true, |
||||
label: 'GridFS Storage Enabled' |
||||
}, |
||||
|
||||
'storageConfig.s3': { |
||||
type: Object, |
||||
optional: true, |
||||
label: 'S3 Configuration' |
||||
}, |
||||
|
||||
'storageConfig.s3.enabled': { |
||||
type: Boolean, |
||||
defaultValue: false, |
||||
label: 'S3 Storage Enabled' |
||||
}, |
||||
|
||||
'storageConfig.s3.endpoint': { |
||||
type: String, |
||||
optional: true, |
||||
label: 'S3 Endpoint' |
||||
}, |
||||
|
||||
'storageConfig.s3.bucket': { |
||||
type: String, |
||||
optional: true, |
||||
label: 'S3 Bucket' |
||||
}, |
||||
|
||||
'storageConfig.s3.region': { |
||||
type: String, |
||||
optional: true, |
||||
label: 'S3 Region' |
||||
}, |
||||
|
||||
'storageConfig.s3.sslEnabled': { |
||||
type: Boolean, |
||||
defaultValue: true, |
||||
label: 'S3 SSL Enabled' |
||||
}, |
||||
|
||||
'storageConfig.s3.port': { |
||||
type: Number, |
||||
defaultValue: 443, |
||||
label: 'S3 Port' |
||||
}, |
||||
|
||||
// Upload settings
|
||||
uploadSettings: { |
||||
type: Object, |
||||
optional: true, |
||||
label: 'Upload Settings' |
||||
}, |
||||
|
||||
'uploadSettings.maxFileSize': { |
||||
type: Number, |
||||
optional: true, |
||||
label: 'Maximum File Size (bytes)' |
||||
}, |
||||
|
||||
'uploadSettings.allowedMimeTypes': { |
||||
type: Array, |
||||
optional: true, |
||||
label: 'Allowed MIME Types' |
||||
}, |
||||
|
||||
'uploadSettings.allowedMimeTypes.$': { |
||||
type: String, |
||||
label: 'MIME Type' |
||||
}, |
||||
|
||||
// Migration settings
|
||||
migrationSettings: { |
||||
type: Object, |
||||
optional: true, |
||||
label: 'Migration Settings' |
||||
}, |
||||
|
||||
'migrationSettings.autoMigrate': { |
||||
type: Boolean, |
||||
defaultValue: false, |
||||
label: 'Auto Migrate to Default Storage' |
||||
}, |
||||
|
||||
'migrationSettings.batchSize': { |
||||
type: Number, |
||||
defaultValue: 10, |
||||
min: 1, |
||||
max: 100, |
||||
label: 'Migration Batch Size' |
||||
}, |
||||
|
||||
'migrationSettings.delayMs': { |
||||
type: Number, |
||||
defaultValue: 1000, |
||||
min: 100, |
||||
max: 10000, |
||||
label: 'Migration Delay (ms)' |
||||
}, |
||||
|
||||
'migrationSettings.cpuThreshold': { |
||||
type: Number, |
||||
defaultValue: 70, |
||||
min: 10, |
||||
max: 90, |
||||
label: 'CPU Threshold (%)' |
||||
}, |
||||
|
||||
// Metadata
|
||||
createdAt: { |
||||
type: Date, |
||||
autoValue() { |
||||
if (this.isInsert) { |
||||
return new Date(); |
||||
} else if (this.isUpsert) { |
||||
return { $setOnInsert: new Date() }; |
||||
} else { |
||||
this.unset(); |
||||
} |
||||
}, |
||||
label: 'Created At' |
||||
}, |
||||
|
||||
updatedAt: { |
||||
type: Date, |
||||
autoValue() { |
||||
if (this.isUpdate || this.isUpsert) { |
||||
return new Date(); |
||||
} |
||||
}, |
||||
label: 'Updated At' |
||||
}, |
||||
|
||||
createdBy: { |
||||
type: String, |
||||
optional: true, |
||||
label: 'Created By' |
||||
}, |
||||
|
||||
updatedBy: { |
||||
type: String, |
||||
optional: true, |
||||
label: 'Updated By' |
||||
} |
||||
}) |
||||
); |
||||
|
||||
// Helper methods
|
||||
AttachmentStorageSettings.helpers({ |
||||
// Get default storage backend
|
||||
getDefaultStorage() { |
||||
return this.defaultStorage || STORAGE_NAME_FILESYSTEM; |
||||
}, |
||||
|
||||
// Check if storage backend is enabled
|
||||
isStorageEnabled(storageName) { |
||||
if (!this.storageConfig) return false; |
||||
|
||||
switch (storageName) { |
||||
case STORAGE_NAME_FILESYSTEM: |
||||
return this.storageConfig.filesystem?.enabled !== false; |
||||
case STORAGE_NAME_GRIDFS: |
||||
return this.storageConfig.gridfs?.enabled !== false; |
||||
case STORAGE_NAME_S3: |
||||
return this.storageConfig.s3?.enabled === true; |
||||
default: |
||||
return false; |
||||
} |
||||
}, |
||||
|
||||
// Get storage configuration
|
||||
getStorageConfig(storageName) { |
||||
if (!this.storageConfig) return null; |
||||
|
||||
switch (storageName) { |
||||
case STORAGE_NAME_FILESYSTEM: |
||||
return this.storageConfig.filesystem; |
||||
case STORAGE_NAME_GRIDFS: |
||||
return this.storageConfig.gridfs; |
||||
case STORAGE_NAME_S3: |
||||
return this.storageConfig.s3; |
||||
default: |
||||
return null; |
||||
} |
||||
}, |
||||
|
||||
// Get upload settings
|
||||
getUploadSettings() { |
||||
return this.uploadSettings || {}; |
||||
}, |
||||
|
||||
// Get migration settings
|
||||
getMigrationSettings() { |
||||
return this.migrationSettings || {}; |
||||
} |
||||
}); |
||||
|
||||
// Server-side methods
|
||||
if (Meteor.isServer) { |
||||
// Get or create default settings
|
||||
Meteor.methods({ |
||||
'getAttachmentStorageSettings'() { |
||||
if (!this.userId) { |
||||
throw new Meteor.Error('not-authorized', 'Must be logged in'); |
||||
} |
||||
|
||||
const user = ReactiveCache.getUser(this.userId); |
||||
if (!user || !user.isAdmin) { |
||||
throw new Meteor.Error('not-authorized', 'Admin access required'); |
||||
} |
||||
|
||||
let settings = AttachmentStorageSettings.findOne({}); |
||||
|
||||
if (!settings) { |
||||
// Create default settings
|
||||
settings = { |
||||
defaultStorage: STORAGE_NAME_FILESYSTEM, |
||||
storageConfig: { |
||||
filesystem: { |
||||
enabled: true, |
||||
path: process.env.WRITABLE_PATH ? `${process.env.WRITABLE_PATH}/attachments` : '/data/attachments' |
||||
}, |
||||
gridfs: { |
||||
enabled: true |
||||
}, |
||||
s3: { |
||||
enabled: false |
||||
} |
||||
}, |
||||
uploadSettings: { |
||||
maxFileSize: process.env.ATTACHMENTS_UPLOAD_MAX_SIZE ? parseInt(process.env.ATTACHMENTS_UPLOAD_MAX_SIZE) : 0, |
||||
allowedMimeTypes: process.env.ATTACHMENTS_UPLOAD_MIME_TYPES ? process.env.ATTACHMENTS_UPLOAD_MIME_TYPES.split(',').map(t => t.trim()) : [] |
||||
}, |
||||
migrationSettings: { |
||||
autoMigrate: false, |
||||
batchSize: 10, |
||||
delayMs: 1000, |
||||
cpuThreshold: 70 |
||||
}, |
||||
createdBy: this.userId, |
||||
updatedBy: this.userId |
||||
}; |
||||
|
||||
AttachmentStorageSettings.insert(settings); |
||||
settings = AttachmentStorageSettings.findOne({}); |
||||
} |
||||
|
||||
return settings; |
||||
}, |
||||
|
||||
'updateAttachmentStorageSettings'(settings) { |
||||
if (!this.userId) { |
||||
throw new Meteor.Error('not-authorized', 'Must be logged in'); |
||||
} |
||||
|
||||
const user = ReactiveCache.getUser(this.userId); |
||||
if (!user || !user.isAdmin) { |
||||
throw new Meteor.Error('not-authorized', 'Admin access required'); |
||||
} |
||||
|
||||
// Validate settings
|
||||
const schema = AttachmentStorageSettings.simpleSchema(); |
||||
schema.validate(settings); |
||||
|
||||
// Update settings
|
||||
const result = AttachmentStorageSettings.upsert( |
||||
{}, |
||||
{ |
||||
$set: { |
||||
...settings, |
||||
updatedBy: this.userId, |
||||
updatedAt: new Date() |
||||
} |
||||
} |
||||
); |
||||
|
||||
return result; |
||||
}, |
||||
|
||||
'getDefaultAttachmentStorage'() { |
||||
if (!this.userId) { |
||||
throw new Meteor.Error('not-authorized', 'Must be logged in'); |
||||
} |
||||
|
||||
const settings = AttachmentStorageSettings.findOne({}); |
||||
return settings ? settings.getDefaultStorage() : STORAGE_NAME_FILESYSTEM; |
||||
}, |
||||
|
||||
'setDefaultAttachmentStorage'(storageName) { |
||||
if (!this.userId) { |
||||
throw new Meteor.Error('not-authorized', 'Must be logged in'); |
||||
} |
||||
|
||||
const user = ReactiveCache.getUser(this.userId); |
||||
if (!user || !user.isAdmin) { |
||||
throw new Meteor.Error('not-authorized', 'Admin access required'); |
||||
} |
||||
|
||||
if (![STORAGE_NAME_FILESYSTEM, STORAGE_NAME_GRIDFS, STORAGE_NAME_S3].includes(storageName)) { |
||||
throw new Meteor.Error('invalid-storage', 'Invalid storage backend'); |
||||
} |
||||
|
||||
const result = AttachmentStorageSettings.upsert( |
||||
{}, |
||||
{ |
||||
$set: { |
||||
defaultStorage: storageName, |
||||
updatedBy: this.userId, |
||||
updatedAt: new Date() |
||||
} |
||||
} |
||||
); |
||||
|
||||
return result; |
||||
} |
||||
}); |
||||
|
||||
// Publication for settings
|
||||
Meteor.publish('attachmentStorageSettings', function() { |
||||
if (!this.userId) { |
||||
return this.ready(); |
||||
} |
||||
|
||||
const user = ReactiveCache.getUser(this.userId); |
||||
if (!user || !user.isAdmin) { |
||||
return this.ready(); |
||||
} |
||||
|
||||
return AttachmentStorageSettings.find({}); |
||||
}); |
||||
} |
||||
|
||||
export default AttachmentStorageSettings; |
||||
@ -0,0 +1,468 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
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 methods
|
||||
if (Meteor.isServer) { |
||||
Meteor.methods({ |
||||
// Upload attachment via API
|
||||
'api.attachment.upload'(boardId, swimlaneId, listId, cardId, fileData, fileName, fileType, storageBackend) { |
||||
if (!this.userId) { |
||||
throw new Meteor.Error('not-authorized', 'Must be logged in'); |
||||
} |
||||
|
||||
// Validate parameters
|
||||
if (!boardId || !swimlaneId || !listId || !cardId || !fileData || !fileName) { |
||||
throw new Meteor.Error('invalid-parameters', 'Missing required parameters'); |
||||
} |
||||
|
||||
// Check if user has permission to modify the card
|
||||
const card = ReactiveCache.getCard(cardId); |
||||
if (!card) { |
||||
throw new Meteor.Error('card-not-found', 'Card not found'); |
||||
} |
||||
|
||||
const board = ReactiveCache.getBoard(boardId); |
||||
if (!board) { |
||||
throw new Meteor.Error('board-not-found', 'Board not found'); |
||||
} |
||||
|
||||
// Check permissions
|
||||
if (!board.isBoardMember(this.userId)) { |
||||
throw new Meteor.Error('not-authorized', 'You do not have permission to modify this card'); |
||||
} |
||||
|
||||
// Check if board allows attachments
|
||||
if (!board.allowsAttachments) { |
||||
throw new Meteor.Error('attachments-not-allowed', '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)) { |
||||
throw new Meteor.Error('invalid-storage', 'Invalid storage backend'); |
||||
} |
||||
|
||||
try { |
||||
// 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); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
return { |
||||
success: true, |
||||
attachmentId: uploader._id, |
||||
fileName: fileName, |
||||
fileSize: fileBuffer.length, |
||||
storageBackend: targetStorage, |
||||
message: 'Attachment uploaded successfully' |
||||
}; |
||||
} else { |
||||
throw new Meteor.Error('upload-failed', 'Failed to upload attachment'); |
||||
} |
||||
} catch (error) { |
||||
console.error('API attachment upload error:', error); |
||||
throw new Meteor.Error('upload-error', error.message); |
||||
} |
||||
}, |
||||
|
||||
// Download attachment via API
|
||||
'api.attachment.download'(attachmentId) { |
||||
if (!this.userId) { |
||||
throw new Meteor.Error('not-authorized', 'Must be logged in'); |
||||
} |
||||
|
||||
// Get attachment
|
||||
const attachment = ReactiveCache.getAttachment(attachmentId); |
||||
if (!attachment) { |
||||
throw new Meteor.Error('attachment-not-found', 'Attachment not found'); |
||||
} |
||||
|
||||
// Check permissions
|
||||
const board = ReactiveCache.getBoard(attachment.meta.boardId); |
||||
if (!board || !board.isBoardMember(this.userId)) { |
||||
throw new Meteor.Error('not-authorized', 'You do not have permission to access this attachment'); |
||||
} |
||||
|
||||
try { |
||||
// Get file strategy
|
||||
const strategy = fileStoreStrategyFactory.getFileStrategy(attachment, 'original'); |
||||
const readStream = strategy.getReadStream(); |
||||
|
||||
if (!readStream) { |
||||
throw new Meteor.Error('file-not-found', 'File not found in storage'); |
||||
} |
||||
|
||||
// Read file data
|
||||
const chunks = []; |
||||
return new Promise((resolve, reject) => { |
||||
readStream.on('data', (chunk) => { |
||||
chunks.push(chunk); |
||||
}); |
||||
|
||||
readStream.on('end', () => { |
||||
const fileBuffer = Buffer.concat(chunks); |
||||
const base64Data = fileBuffer.toString('base64'); |
||||
|
||||
resolve({ |
||||
success: true, |
||||
attachmentId: attachmentId, |
||||
fileName: attachment.name, |
||||
fileSize: attachment.size, |
||||
fileType: attachment.type, |
||||
base64Data: base64Data, |
||||
storageBackend: strategy.getStorageName() |
||||
}); |
||||
}); |
||||
|
||||
readStream.on('error', (error) => { |
||||
reject(new Meteor.Error('download-error', error.message)); |
||||
}); |
||||
}); |
||||
} catch (error) { |
||||
console.error('API attachment download error:', error); |
||||
throw new Meteor.Error('download-error', error.message); |
||||
} |
||||
}, |
||||
|
||||
// List attachments for board, swimlane, list, or card
|
||||
'api.attachment.list'(boardId, swimlaneId, listId, cardId) { |
||||
if (!this.userId) { |
||||
throw new Meteor.Error('not-authorized', 'Must be logged in'); |
||||
} |
||||
|
||||
// Check permissions
|
||||
const board = ReactiveCache.getBoard(boardId); |
||||
if (!board || !board.isBoardMember(this.userId)) { |
||||
throw new Meteor.Error('not-authorized', 'You do not have permission to access this board'); |
||||
} |
||||
|
||||
try { |
||||
let query = { 'meta.boardId': boardId }; |
||||
|
||||
if (swimlaneId) { |
||||
query['meta.swimlaneId'] = swimlaneId; |
||||
} |
||||
|
||||
if (listId) { |
||||
query['meta.listId'] = listId; |
||||
} |
||||
|
||||
if (cardId) { |
||||
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 |
||||
}; |
||||
}); |
||||
|
||||
return { |
||||
success: true, |
||||
attachments: attachmentList, |
||||
count: attachmentList.length |
||||
}; |
||||
} catch (error) { |
||||
console.error('API attachment list error:', error); |
||||
throw new Meteor.Error('list-error', error.message); |
||||
} |
||||
}, |
||||
|
||||
// Copy attachment to another card
|
||||
'api.attachment.copy'(attachmentId, targetBoardId, targetSwimlaneId, targetListId, targetCardId) { |
||||
if (!this.userId) { |
||||
throw new Meteor.Error('not-authorized', 'Must be logged in'); |
||||
} |
||||
|
||||
// Get source attachment
|
||||
const sourceAttachment = ReactiveCache.getAttachment(attachmentId); |
||||
if (!sourceAttachment) { |
||||
throw new Meteor.Error('attachment-not-found', 'Source attachment not found'); |
||||
} |
||||
|
||||
// Check source permissions
|
||||
const sourceBoard = ReactiveCache.getBoard(sourceAttachment.meta.boardId); |
||||
if (!sourceBoard || !sourceBoard.isBoardMember(this.userId)) { |
||||
throw new Meteor.Error('not-authorized', 'You do not have permission to access the source attachment'); |
||||
} |
||||
|
||||
// Check target permissions
|
||||
const targetBoard = ReactiveCache.getBoard(targetBoardId); |
||||
if (!targetBoard || !targetBoard.isBoardMember(this.userId)) { |
||||
throw new Meteor.Error('not-authorized', 'You do not have permission to modify the target card'); |
||||
} |
||||
|
||||
// Check if target board allows attachments
|
||||
if (!targetBoard.allowsAttachments) { |
||||
throw new Meteor.Error('attachments-not-allowed', 'Attachments are not allowed on the target board'); |
||||
} |
||||
|
||||
try { |
||||
// Get source file strategy
|
||||
const sourceStrategy = fileStoreStrategyFactory.getFileStrategy(sourceAttachment, 'original'); |
||||
const readStream = sourceStrategy.getReadStream(); |
||||
|
||||
if (!readStream) { |
||||
throw new Meteor.Error('file-not-found', 'Source file not found in storage'); |
||||
} |
||||
|
||||
// Read source file data
|
||||
const chunks = []; |
||||
return new Promise((resolve, reject) => { |
||||
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) { |
||||
resolve({ |
||||
success: true, |
||||
sourceAttachmentId: attachmentId, |
||||
newAttachmentId: uploader._id, |
||||
fileName: sourceAttachment.name, |
||||
fileSize: sourceAttachment.size, |
||||
message: 'Attachment copied successfully' |
||||
}); |
||||
} else { |
||||
reject(new Meteor.Error('copy-failed', 'Failed to copy attachment')); |
||||
} |
||||
} catch (error) { |
||||
reject(new Meteor.Error('copy-error', error.message)); |
||||
} |
||||
}); |
||||
|
||||
readStream.on('error', (error) => { |
||||
reject(new Meteor.Error('copy-error', error.message)); |
||||
}); |
||||
}); |
||||
} catch (error) { |
||||
console.error('API attachment copy error:', error); |
||||
throw new Meteor.Error('copy-error', error.message); |
||||
} |
||||
}, |
||||
|
||||
// Move attachment to another card
|
||||
'api.attachment.move'(attachmentId, targetBoardId, targetSwimlaneId, targetListId, targetCardId) { |
||||
if (!this.userId) { |
||||
throw new Meteor.Error('not-authorized', 'Must be logged in'); |
||||
} |
||||
|
||||
// Get source attachment
|
||||
const sourceAttachment = ReactiveCache.getAttachment(attachmentId); |
||||
if (!sourceAttachment) { |
||||
throw new Meteor.Error('attachment-not-found', 'Source attachment not found'); |
||||
} |
||||
|
||||
// Check source permissions
|
||||
const sourceBoard = ReactiveCache.getBoard(sourceAttachment.meta.boardId); |
||||
if (!sourceBoard || !sourceBoard.isBoardMember(this.userId)) { |
||||
throw new Meteor.Error('not-authorized', 'You do not have permission to access the source attachment'); |
||||
} |
||||
|
||||
// Check target permissions
|
||||
const targetBoard = ReactiveCache.getBoard(targetBoardId); |
||||
if (!targetBoard || !targetBoard.isBoardMember(this.userId)) { |
||||
throw new Meteor.Error('not-authorized', 'You do not have permission to modify the target card'); |
||||
} |
||||
|
||||
// Check if target board allows attachments
|
||||
if (!targetBoard.allowsAttachments) { |
||||
throw new Meteor.Error('attachments-not-allowed', 'Attachments are not allowed on the target board'); |
||||
} |
||||
|
||||
try { |
||||
// 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() |
||||
} |
||||
}); |
||||
|
||||
return { |
||||
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); |
||||
throw new Meteor.Error('move-error', error.message); |
||||
} |
||||
}, |
||||
|
||||
// Delete attachment via API
|
||||
'api.attachment.delete'(attachmentId) { |
||||
if (!this.userId) { |
||||
throw new Meteor.Error('not-authorized', 'Must be logged in'); |
||||
} |
||||
|
||||
// Get attachment
|
||||
const attachment = ReactiveCache.getAttachment(attachmentId); |
||||
if (!attachment) { |
||||
throw new Meteor.Error('attachment-not-found', 'Attachment not found'); |
||||
} |
||||
|
||||
// Check permissions
|
||||
const board = ReactiveCache.getBoard(attachment.meta.boardId); |
||||
if (!board || !board.isBoardMember(this.userId)) { |
||||
throw new Meteor.Error('not-authorized', 'You do not have permission to delete this attachment'); |
||||
} |
||||
|
||||
try { |
||||
// Delete attachment
|
||||
Attachments.remove(attachmentId); |
||||
|
||||
return { |
||||
success: true, |
||||
attachmentId: attachmentId, |
||||
fileName: attachment.name, |
||||
message: 'Attachment deleted successfully' |
||||
}; |
||||
} catch (error) { |
||||
console.error('API attachment delete error:', error); |
||||
throw new Meteor.Error('delete-error', error.message); |
||||
} |
||||
}, |
||||
|
||||
// Get attachment info via API
|
||||
'api.attachment.info'(attachmentId) { |
||||
if (!this.userId) { |
||||
throw new Meteor.Error('not-authorized', 'Must be logged in'); |
||||
} |
||||
|
||||
// Get attachment
|
||||
const attachment = ReactiveCache.getAttachment(attachmentId); |
||||
if (!attachment) { |
||||
throw new Meteor.Error('attachment-not-found', 'Attachment not found'); |
||||
} |
||||
|
||||
// Check permissions
|
||||
const board = ReactiveCache.getBoard(attachment.meta.boardId); |
||||
if (!board || !board.isBoardMember(this.userId)) { |
||||
throw new Meteor.Error('not-authorized', 'You do not have permission to access this attachment'); |
||||
} |
||||
|
||||
try { |
||||
const strategy = fileStoreStrategyFactory.getFileStrategy(attachment, 'original'); |
||||
|
||||
return { |
||||
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) { |
||||
console.error('API attachment info error:', error); |
||||
throw new Meteor.Error('info-error', error.message); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
@ -0,0 +1,553 @@ |
||||
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); |
||||
} |
||||
}); |
||||
} |
||||
Loading…
Reference in new issue