import { Meteor } from 'meteor/meteor' ;
import { FilesCollection } from 'meteor/ostrio:files' ;
import { exec } from 'node:child_process' ;
import { promisify } from 'node:util' ;
import { createBucket } from './lib/grid/createBucket' ;
import fs from 'fs' ;
import FileType from 'file-type' ;
import path from 'path' ;
import { AttachmentStoreStrategyFilesystem , AttachmentStoreStrategyGridFs } from '/models/lib/attachmentStoreStrategy' ;
import FileStoreStrategyFactory , { moveToStorage , rename , STORAGE _NAME _FILESYSTEM , STORAGE _NAME _GRIDFS } from '/models/lib/fileStoreStrategy' ;
let asyncExec ;
let attachmentUploadExternalProgram ;
let attachmentUploadMimeTypes = [ ] ;
let attachmentUploadSize = 0 ;
let attachmentBucket ;
let storagePath ;
if ( Meteor . isServer ) {
asyncExec = promisify ( exec ) ;
attachmentBucket = createBucket ( 'attachments' ) ;
if ( process . env . ATTACHMENTS _UPLOAD _MIME _TYPES ) {
attachmentUploadMimeTypes = process . env . ATTACHMENTS _UPLOAD _MIME _TYPES . split ( ',' ) ;
attachmentUploadMimeTypes = attachmentUploadMimeTypes . map ( value => value . trim ( ) ) ;
}
if ( process . env . ATTACHMENTS _UPLOAD _MAX _SIZE ) {
attachmentUploadSize = parseInt ( process . env . ATTACHMENTS _UPLOAD _MAX _SIZE ) ;
if ( isNaN ( attachmentUploadSize ) ) {
attachmentUploadSize = 0
}
}
if ( process . env . ATTACHMENTS _UPLOAD _EXTERNAL _PROGRAM ) {
attachmentUploadExternalProgram = process . env . ATTACHMENTS _UPLOAD _EXTERNAL _PROGRAM ;
if ( ! attachmentUploadExternalProgram . includes ( "{file}" ) ) {
attachmentUploadExternalProgram = undefined ;
}
}
storagePath = path . join ( process . env . WRITABLE _PATH , 'attachments' ) ;
}
export const fileStoreStrategyFactory = new FileStoreStrategyFactory ( AttachmentStoreStrategyFilesystem , storagePath , AttachmentStoreStrategyGridFs , attachmentBucket ) ;
// XXX Enforce a schema for the Attachments FilesCollection
// see: https://github.com/VeliovGroup/Meteor-Files/wiki/Schema
Attachments = new FilesCollection ( {
debug : false , // Change to `true` for debugging
collectionName : 'attachments' ,
allowClientCode : true ,
namingFunction ( opts ) {
let filenameWithoutExtension = ""
let fileId = "" ;
if ( opts ? . name ) {
// Client
filenameWithoutExtension = opts . name . replace ( /(.+)\..+/ , "$1" ) ;
fileId = opts . meta . fileId ;
delete opts . meta . fileId ;
} else if ( opts ? . file ? . name ) {
// Server
filenameWithoutExtension = opts . file . name . replace ( new RegExp ( opts . file . extensionWithDot + "$" ) , "" )
fileId = opts . fileId ;
}
else {
// should never reach here
filenameWithoutExtension = Math . random ( ) . toString ( 36 ) . slice ( 2 ) ;
fileId = Math . random ( ) . toString ( 36 ) . slice ( 2 ) ;
}
const ret = fileId + "-original-" + filenameWithoutExtension ;
// remove fileId from meta, it was only stored there to have this information here in the namingFunction function
return ret ;
} ,
storagePath ( ) {
const ret = fileStoreStrategyFactory . storagePath ;
return ret ;
} ,
onAfterUpload ( fileObj ) {
// current storage is the filesystem, update object and database
Object . keys ( fileObj . versions ) . forEach ( versionName => {
fileObj . versions [ versionName ] . storage = STORAGE _NAME _FILESYSTEM ;
} ) ;
Attachments . update ( { _id : fileObj . _id } , { $set : { "versions" : fileObj . versions } } ) ;
let storageDestination = fileObj . meta . copyStorage || STORAGE _NAME _GRIDFS ;
Meteor . defer ( ( ) => Meteor . call ( 'validateAttachmentAndMoveToStorage' , fileObj . _id , storageDestination ) ) ;
} ,
interceptDownload ( http , fileObj , versionName ) {
const ret = fileStoreStrategyFactory . getFileStrategy ( fileObj , versionName ) . interceptDownload ( http , this . cacheControl ) ;
return ret ;
} ,
onAfterRemove ( files ) {
files . forEach ( fileObj => {
Object . keys ( fileObj . versions ) . forEach ( versionName => {
fileStoreStrategyFactory . getFileStrategy ( fileObj , versionName ) . onAfterRemove ( ) ;
} ) ;
} ) ;
} ,
// We authorize the attachment download either:
// - if the board is public, everyone (even unconnected) can download it
// - if the board is private, only board members can download it
protected ( fileObj ) {
// file may have been deleted already again after upload validation failed
if ( ! fileObj ) {
return false ;
}
const board = Boards . findOne ( fileObj . meta . boardId ) ;
if ( board . isPublic ( ) ) {
return true ;
}
return board . hasMember ( this . userId ) ;
} ,
} ) ;
if ( Meteor . isServer ) {
Attachments . allow ( {
insert ( userId , fileObj ) {
return allowIsBoardMember ( userId , Boards . findOne ( fileObj . boardId ) ) ;
} ,
update ( userId , fileObj ) {
return allowIsBoardMember ( userId , Boards . findOne ( fileObj . boardId ) ) ;
} ,
remove ( userId , fileObj ) {
return allowIsBoardMember ( userId , Boards . findOne ( fileObj . boardId ) ) ;
} ,
fetch : [ 'meta' ] ,
} ) ;
Meteor . methods ( {
moveAttachmentToStorage ( fileObjId , storageDestination ) {
check ( fileObjId , String ) ;
check ( storageDestination , String ) ;
const fileObj = Attachments . findOne ( { _id : fileObjId } ) ;
moveToStorage ( fileObj , storageDestination , fileStoreStrategyFactory ) ;
// since Meteor-Files 2.1.0 the filename is truncated to 28 characters, so rename the file after upload to the right filename back
rename ( fileObj , fileObj . name , fileStoreStrategyFactory ) ;
} ,
renameAttachment ( fileObjId , newName ) {
check ( fileObjId , String ) ;
check ( newName , String ) ;
const fileObj = Attachments . findOne ( { _id : fileObjId } ) ;
rename ( fileObj , newName , fileStoreStrategyFactory ) ;
} ,
validateAttachment ( fileObjId ) {
check ( fileObjId , String ) ;
const fileObj = Attachments . findOne ( { _id : fileObjId } ) ;
let isValid = true ;
if ( attachmentUploadMimeTypes . length ) {
const mimeTypeResult = Promise . await ( FileType . fromFile ( fileObj . path ) ) ;
const mimeType = ( mimeTypeResult ? mimeTypeResult . mime : fileObj . type ) ;
const baseMimeType = mimeType . split ( '/' , 1 ) [ 0 ] ;
isValid = attachmentUploadMimeTypes . includes ( mimeType ) || attachmentUploadMimeTypes . includes ( baseMimeType + '/*' ) || attachmentUploadMimeTypes . includes ( '*' ) ;
if ( ! isValid ) {
console . log ( "Validation of uploaded file failed: file " + fileObj . path + " - mimetype " + mimeType ) ;
}
}
if ( attachmentUploadSize && fileObj . size > attachmentUploadSize ) {
console . log ( "Validation of uploaded file failed: file " + fileObj . path + " - size " + fileObj . size ) ;
isValid = false ;
}
if ( isValid && attachmentUploadExternalProgram ) {
Promise . await ( asyncExec ( attachmentUploadExternalProgram . replace ( "{file}" , '"' + fileObj . path + '"' ) ) ) ;
isValid = fs . existsSync ( fileObj . path ) ;
if ( ! isValid ) {
console . log ( "Validation of uploaded file failed: file " + fileObj . path + " has been deleted externally" ) ;
}
}
if ( ! isValid ) {
Attachments . remove ( fileObjId ) ;
}
} ,
validateAttachmentAndMoveToStorage ( fileObjId , storageDestination ) {
check ( fileObjId , String ) ;
check ( storageDestination , String ) ;
validateAttachment ( fileObjId ) ;
const fileObj = Attachments . findOne ( { _id : fileObjId } ) ;
if ( fileObj ) {
console . debug ( "Validation of uploaded file completed: file " + fileObj . path + " - storage destination " + storageDestination ) ;
moveAttachmentToStorage ( fileObjId , storageDestination ) ;
}
} ,
} ) ;
Meteor . startup ( ( ) => {
Attachments . collection . createIndex ( { 'meta.cardId' : 1 } ) ;
const storagePath = fileStoreStrategyFactory . storagePath ;
if ( ! fs . existsSync ( storagePath ) ) {
console . log ( "create storagePath because it doesn't exist: " + storagePath ) ;
fs . mkdirSync ( storagePath , { recursive : true } ) ;
}
} ) ;
}
export default Attachments ;