import { ReactiveCache } from '/imports/reactiveCache' ;
import { Meteor } from 'meteor/meteor' ;
import { FilesCollection } from 'meteor/ostrio:files' ;
import { isFileValid } from './fileValidation' ;
import { createBucket } from './lib/grid/createBucket' ;
import fs from 'fs' ;
import path from 'path' ;
import { AttachmentStoreStrategyFilesystem , AttachmentStoreStrategyGridFs , AttachmentStoreStrategyS3 } from '/models/lib/attachmentStoreStrategy' ;
import FileStoreStrategyFactory , { moveToStorage , rename , STORAGE _NAME _FILESYSTEM , STORAGE _NAME _GRIDFS , STORAGE _NAME _S3 } from '/models/lib/fileStoreStrategy' ;
let attachmentUploadExternalProgram ;
let attachmentUploadMimeTypes = [ ] ;
let attachmentUploadSize = 0 ;
let attachmentBucket ;
let storagePath ;
if ( Meteor . isServer ) {
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
if ( opts . file . extension ) {
filenameWithoutExtension = opts . file . name . replace ( new RegExp ( opts . file . extensionWithDot + "$" ) , "" )
} else {
// file has no extension, so don't replace anything, otherwise the last character is removed (because extensionWithDot = '.')
filenameWithoutExtension = opts . file . name ;
}
fileId = opts . fileId ;
}
else {
// should never reach here
filenameWithoutExtension = Math . random ( ) . toString ( 36 ) . slice ( 2 ) ;
fileId = Math . random ( ) . toString ( 36 ) . slice ( 2 ) ;
}
// OLD:
//const ret = fileId + "-original-" + filenameWithoutExtension;
// NEW: Save file only with filename of ObjectID, not including filename.
// Fixes https://github.com/wekan/wekan/issues/4416#issuecomment-1510517168
const ret = fileId ;
// remove fileId from meta, it was only stored there to have this information here in the namingFunction function
return ret ;
} ,
sanitize ( str , max , replacement ) {
// keep the original filename
return str ;
} ,
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 ;
} ) ;
this . _now = new Date ( ) ;
Attachments . update ( { _id : fileObj . _id } , { $set : { "versions" : fileObj . versions } } ) ;
Attachments . update ( { _id : fileObj . uploadedAtOstrio } , { $set : { "uploadedAtOstrio" : this . _now } } ) ;
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 = ReactiveCache . getBoard ( fileObj . meta . boardId ) ;
if ( board . isPublic ( ) ) {
return true ;
}
return board . hasMember ( this . userId ) ;
} ,
} ) ;
if ( Meteor . isServer ) {
Attachments . allow ( {
insert ( userId , fileObj ) {
return allowIsBoardMember ( userId , ReactiveCache . getBoard ( fileObj . boardId ) ) ;
} ,
update ( userId , fileObj ) {
return allowIsBoardMember ( userId , ReactiveCache . getBoard ( fileObj . boardId ) ) ;
} ,
remove ( userId , fileObj ) {
return allowIsBoardMember ( userId , ReactiveCache . getBoard ( fileObj . boardId ) ) ;
} ,
fetch : [ 'meta' ] ,
} ) ;
Meteor . methods ( {
moveAttachmentToStorage ( fileObjId , storageDestination ) {
check ( fileObjId , String ) ;
check ( storageDestination , String ) ;
const fileObj = ReactiveCache . getAttachment ( fileObjId ) ;
moveToStorage ( fileObj , storageDestination , fileStoreStrategyFactory ) ;
} ,
renameAttachment ( fileObjId , newName ) {
check ( fileObjId , String ) ;
check ( newName , String ) ;
const fileObj = ReactiveCache . getAttachment ( fileObjId ) ;
rename ( fileObj , newName , fileStoreStrategyFactory ) ;
} ,
validateAttachment ( fileObjId ) {
check ( fileObjId , String ) ;
const fileObj = ReactiveCache . getAttachment ( fileObjId ) ;
const isValid = Promise . await ( isFileValid ( fileObj , attachmentUploadMimeTypes , attachmentUploadSize , attachmentUploadExternalProgram ) ) ;
if ( ! isValid ) {
Attachments . remove ( fileObjId ) ;
}
} ,
validateAttachmentAndMoveToStorage ( fileObjId , storageDestination ) {
check ( fileObjId , String ) ;
check ( storageDestination , String ) ;
Meteor . call ( 'validateAttachment' , fileObjId ) ;
const fileObj = ReactiveCache . getAttachment ( fileObjId ) ;
if ( fileObj ) {
Meteor . defer ( ( ) => Meteor . call ( '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 ;