import fs from 'fs' ;
import path from 'path' ;
import { createObjectId } from './grid/createObjectId' ;
import { httpStreamOutput } from './httpStream.js'
export const STORAGE _NAME _FILESYSTEM = "fs" ;
export const STORAGE _NAME _GRIDFS = "gridfs" ;
/** Factory for FileStoreStrategy */
export default class FileStoreStrategyFactory {
/ * * c o n s t r u c t o r
* @ param classFileStoreStrategyFilesystem use this strategy for filesystem storage
* @ param storagePath file storage path
* @ param classFileStoreStrategyGridFs use this strategy for GridFS storage
* @ param gridFsBucket use this GridFS Bucket as GridFS Storage
* /
constructor ( classFileStoreStrategyFilesystem , storagePath , classFileStoreStrategyGridFs , gridFsBucket ) {
this . classFileStoreStrategyFilesystem = classFileStoreStrategyFilesystem ;
this . storagePath = storagePath ;
this . classFileStoreStrategyGridFs = classFileStoreStrategyGridFs ;
this . gridFsBucket = gridFsBucket ;
}
/ * * r e t u r n s t h e r i g h t F i l e S t o r e S t r a t e g y
* @ param fileObj the current file object
* @ param versionName the current version
* @ param use this storage , or if not set , get the storage from fileObj
* /
getFileStrategy ( fileObj , versionName , storage ) {
if ( ! storage ) {
storage = fileObj . versions [ versionName ] . storage ;
if ( ! storage ) {
if ( fileObj . meta . source == "import" || fileObj . versions [ versionName ] . meta . gridFsFileId ) {
// uploaded by import, so it's in GridFS (MongoDB)
storage = STORAGE _NAME _GRIDFS ;
} else {
// newly uploaded, so it's at the filesystem
storage = STORAGE _NAME _FILESYSTEM ;
}
}
}
let ret ;
if ( [ STORAGE _NAME _FILESYSTEM , STORAGE _NAME _GRIDFS ] . includes ( storage ) ) {
if ( storage == STORAGE _NAME _FILESYSTEM ) {
ret = new this . classFileStoreStrategyFilesystem ( fileObj , versionName ) ;
} else if ( storage == STORAGE _NAME _GRIDFS ) {
ret = new this . classFileStoreStrategyGridFs ( this . gridFsBucket , fileObj , versionName ) ;
}
}
return ret ;
}
}
/** Strategy to store files */
class FileStoreStrategy {
/ * * c o n s t r u c t o r
* @ param fileObj the current file object
* @ param versionName the current version
* /
constructor ( fileObj , versionName ) {
this . fileObj = fileObj ;
this . versionName = versionName ;
}
/** after successfull upload */
onAfterUpload ( ) {
}
/ * * d o w n l o a d t h e f i l e
* @ param http the current http request
* @ param cacheControl cacheControl of FilesCollection
* /
interceptDownload ( http , cacheControl ) {
}
/** after file remove */
onAfterRemove ( ) {
}
/ * * r e t u r n s a r e a d s t r e a m
* @ return the read stream
* /
getReadStream ( ) {
}
/ * * r e t u r n s a w r i t e s t r e a m
* @ param filePath if set , use this path
* @ return the write stream
* /
getWriteStream ( filePath ) {
}
/ * * w r i t i n g f i n i s h e d
* @ param finishedData the data of the write stream finish event
* /
writeStreamFinished ( finishedData ) {
}
/ * * r e t u r n s t h e n e w f i l e p a t h
* @ param storagePath use this storage path
* @ return the new file path
* /
getNewPath ( storagePath , name ) {
if ( ! _ . isString ( name ) ) {
name = this . fileObj . name ;
}
const ret = path . join ( storagePath , this . fileObj . _id + "-" + this . versionName + "-" + name ) ;
return ret ;
}
/** remove the file */
unlink ( ) {
}
/ * * r e n a m e t h e f i l e ( p h y s i c a l )
* @ li at database the filename is updated after this method
* @ param newFilePath the new file path
* /
rename ( newFilePath ) {
}
/ * * r e t u r n t h e s t o r a g e n a m e
* @ return the storage name
* /
getStorageName ( ) {
}
}
/** Strategy to store attachments at GridFS (MongoDB) */
export class FileStoreStrategyGridFs extends FileStoreStrategy {
/ * * c o n s t r u c t o r
* @ param gridFsBucket use this GridFS Bucket
* @ param fileObj the current file object
* @ param versionName the current version
* /
constructor ( gridFsBucket , fileObj , versionName ) {
super ( fileObj , versionName ) ;
this . gridFsBucket = gridFsBucket ;
}
/ * * d o w n l o a d t h e f i l e
* @ param http the current http request
* @ param cacheControl cacheControl of FilesCollection
* /
interceptDownload ( http , cacheControl ) {
const readStream = this . getReadStream ( ) ;
const downloadFlag = http ? . params ? . query ? . download ;
let ret = false ;
if ( readStream ) {
ret = true ;
httpStreamOutput ( readStream , this . fileObj . name , http , downloadFlag , cacheControl ) ;
}
return ret ;
}
/** after file remove */
onAfterRemove ( ) {
this . unlink ( ) ;
super . onAfterRemove ( ) ;
}
/ * * r e t u r n s a r e a d s t r e a m
* @ return the read stream
* /
getReadStream ( ) {
const gfsId = this . getGridFsObjectId ( ) ;
let ret ;
if ( gfsId ) {
ret = this . gridFsBucket . openDownloadStream ( gfsId ) ;
}
return ret ;
}
/ * * r e t u r n s a w r i t e s t r e a m
* @ param filePath if set , use this path
* @ return the write stream
* /
getWriteStream ( filePath ) {
const fileObj = this . fileObj ;
const versionName = this . versionName ;
const metadata = { ... fileObj . meta , versionName , fileId : fileObj . _id } ;
const ret = this . gridFsBucket . openUploadStream ( this . fileObj . name , {
contentType : fileObj . type || 'binary/octet-stream' ,
metadata ,
} ) ;
return ret ;
}
/ * * w r i t i n g f i n i s h e d
* @ param finishedData the data of the write stream finish event
* /
writeStreamFinished ( finishedData ) {
const gridFsFileIdName = this . getGridFsFileIdName ( ) ;
Attachments . update ( { _id : this . fileObj . _id } , { $set : { [ gridFsFileIdName ] : finishedData . _id . toHexString ( ) , } } ) ;
}
/** remove the file */
unlink ( ) {
const gfsId = this . getGridFsObjectId ( ) ;
if ( gfsId ) {
this . gridFsBucket . delete ( gfsId , err => {
if ( err ) {
console . error ( "error on gfs bucket.delete: " , err ) ;
}
} ) ;
}
const gridFsFileIdName = this . getGridFsFileIdName ( ) ;
Attachments . update ( { _id : this . fileObj . _id } , { $unset : { [ gridFsFileIdName ] : 1 } } ) ;
}
/ * * r e t u r n t h e s t o r a g e n a m e
* @ return the storage name
* /
getStorageName ( ) {
return STORAGE _NAME _GRIDFS ;
}
/ * * r e t u r n s t h e G r i d F S O b j e c t - I d
* @ return the GridFS Object - Id
* /
getGridFsObjectId ( ) {
let ret ;
const gridFsFileId = this . getGridFsFileId ( ) ;
if ( gridFsFileId ) {
ret = createObjectId ( { gridFsFileId } ) ;
}
return ret ;
}
/ * * r e t u r n s t h e G r i d F S O b j e c t - I d
* @ return the GridFS Object - Id
* /
getGridFsFileId ( ) {
const ret = ( this . fileObj . versions [ this . versionName ] . meta || { } )
. gridFsFileId ;
return ret ;
}
/ * * r e t u r n s t h e p r o p e r t y n a m e o f g r i d F s F i l e I d
* @ return the property name of gridFsFileId
* /
getGridFsFileIdName ( ) {
const ret = ` versions. ${ this . versionName } .meta.gridFsFileId ` ;
return ret ;
}
}
/** Strategy to store attachments at filesystem */
export class FileStoreStrategyFilesystem extends FileStoreStrategy {
/ * * c o n s t r u c t o r
* @ param fileObj the current file object
* @ param versionName the current version
* /
constructor ( fileObj , versionName ) {
super ( fileObj , versionName ) ;
}
/ * * r e t u r n s a r e a d s t r e a m
* @ return the read stream
* /
getReadStream ( ) {
const ret = fs . createReadStream ( this . fileObj . versions [ this . versionName ] . path )
return ret ;
}
/ * * r e t u r n s a w r i t e s t r e a m
* @ param filePath if set , use this path
* @ return the write stream
* /
getWriteStream ( filePath ) {
if ( ! _ . isString ( filePath ) ) {
filePath = this . fileObj . versions [ this . versionName ] . path ;
}
const ret = fs . createWriteStream ( filePath ) ;
return ret ;
}
/ * * w r i t i n g f i n i s h e d
* @ param finishedData the data of the write stream finish event
* /
writeStreamFinished ( finishedData ) {
}
/** remove the file */
unlink ( ) {
const filePath = this . fileObj . versions [ this . versionName ] . path ;
fs . unlink ( filePath , ( ) => { } ) ;
}
/ * * r e n a m e t h e f i l e ( p h y s i c a l )
* @ li at database the filename is updated after this method
* @ param newFilePath the new file path
* /
rename ( newFilePath ) {
fs . renameSync ( this . fileObj . versions [ this . versionName ] . path , newFilePath ) ;
}
/ * * r e t u r n t h e s t o r a g e n a m e
* @ return the storage name
* /
getStorageName ( ) {
return STORAGE _NAME _FILESYSTEM ;
}
}
/ * * m o v e t h e f i l e O b j t o a n o t h e r s t o r a g e
* @ param fileObj move this fileObj to another storage
* @ param storageDestination the storage destination ( fs or gridfs )
* @ param fileStoreStrategyFactory get FileStoreStrategy from this factory
* /
export const moveToStorage = function ( fileObj , storageDestination , fileStoreStrategyFactory ) {
Object . keys ( fileObj . versions ) . forEach ( versionName => {
const strategyRead = fileStoreStrategyFactory . getFileStrategy ( fileObj , versionName ) ;
const strategyWrite = fileStoreStrategyFactory . getFileStrategy ( fileObj , versionName , storageDestination ) ;
if ( strategyRead . constructor . name != strategyWrite . constructor . name ) {
const readStream = strategyRead . getReadStream ( ) ;
const filePath = strategyWrite . getNewPath ( fileStoreStrategyFactory . storagePath ) ;
const writeStream = strategyWrite . getWriteStream ( filePath ) ;
writeStream . on ( 'error' , error => {
console . error ( '[writeStream error]: ' , error , fileObj . _id ) ;
} ) ;
readStream . on ( 'error' , error => {
console . error ( '[readStream error]: ' , error , fileObj . _id ) ;
} ) ;
writeStream . on ( 'finish' , Meteor . bindEnvironment ( ( finishedData ) => {
strategyWrite . writeStreamFinished ( finishedData ) ;
} ) ) ;
// https://forums.meteor.com/t/meteor-code-must-always-run-within-a-fiber-try-wrapping-callbacks-that-you-pass-to-non-meteor-libraries-with-meteor-bindenvironmen/40099/8
readStream . on ( 'end' , Meteor . bindEnvironment ( ( ) => {
Attachments . update ( { _id : fileObj . _id } , { $set : {
[ ` versions. ${ versionName } .storage ` ] : strategyWrite . getStorageName ( ) ,
[ ` versions. ${ versionName } .path ` ] : filePath ,
} } ) ;
strategyRead . unlink ( ) ;
} ) ) ;
readStream . pipe ( writeStream ) ;
}
} ) ;
} ;
export const copyFile = function ( fileObj , newCardId , fileStoreStrategyFactory ) {
const versionName = "original" ;
const strategyRead = fileStoreStrategyFactory . getFileStrategy ( fileObj , versionName ) ;
const readStream = strategyRead . getReadStream ( ) ;
const strategyWrite = fileStoreStrategyFactory . getFileStrategy ( fileObj , versionName , STORAGE _NAME _FILESYSTEM ) ;
const tempPath = path . join ( fileStoreStrategyFactory . storagePath , Random . id ( ) + "-" + versionName + "-" + fileObj . name ) ;
const writeStream = strategyWrite . getWriteStream ( tempPath ) ;
writeStream . on ( 'error' , error => {
console . error ( '[writeStream error]: ' , error , fileObj . _id ) ;
} ) ;
readStream . on ( 'error' , error => {
console . error ( '[readStream error]: ' , error , fileObj . _id ) ;
} ) ;
// https://forums.meteor.com/t/meteor-code-must-always-run-within-a-fiber-try-wrapping-callbacks-that-you-pass-to-non-meteor-libraries-with-meteor-bindenvironmen/40099/8
readStream . on ( 'end' , Meteor . bindEnvironment ( ( ) => {
const fileId = Random . id ( ) ;
Attachments . addFile (
tempPath ,
{
fileName : fileObj . name ,
type : fileObj . type ,
meta : {
boardId : fileObj . meta . boardId ,
cardId : newCardId ,
listId : fileObj . meta . listId ,
swimlaneId : fileObj . meta . swimlaneId ,
source : 'copy' ,
copyFrom : fileObj . _id ,
copyStorage : strategyRead . getStorageName ( ) ,
} ,
userId : fileObj . userId ,
size : fileObj . fileSize ,
fileId ,
} ,
( err , fileRef ) => {
if ( err ) {
console . log ( err ) ;
} else {
// Set the userId again
Attachments . update ( { _id : fileRef . _id } , { $set : { userId : fileObj . userId } } ) ;
}
} ,
true ,
) ;
} ) ) ;
readStream . pipe ( writeStream ) ;
} ;
export const rename = function ( fileObj , newName , fileStoreStrategyFactory ) {
Object . keys ( fileObj . versions ) . forEach ( versionName => {
const strategy = fileStoreStrategyFactory . getFileStrategy ( fileObj , versionName ) ;
const newFilePath = strategy . getNewPath ( fileStoreStrategyFactory . storagePath , newName ) ;
strategy . rename ( newFilePath ) ;
Attachments . update ( { _id : fileObj . _id } , { $set : {
"name" : newName ,
[ ` versions. ${ versionName } .path ` ] : newFilePath ,
} } ) ;
} ) ;
} ;