2018-02-13 22:50:13 +00:00
'use strict' ;
const knex = require ( '../lib/knex' ) ;
const { enforce } = require ( '../lib/helpers' ) ;
const dtHelpers = require ( '../lib/dt-helpers' ) ;
const shares = require ( './shares' ) ;
const fs = require ( 'fs-extra-promise' ) ;
const path = require ( 'path' ) ;
2018-11-18 14:38:52 +00:00
const interoperableErrors = require ( '../../shared/interoperable-errors' ) ;
2018-08-03 11:35:55 +00:00
const entitySettings = require ( '../lib/entity-settings' ) ;
2018-09-29 20:07:24 +00:00
const { getPublicUrl } = require ( '../lib/urls' ) ;
2018-04-02 17:05:22 +00:00
2018-08-02 10:19:27 +00:00
const crypto = require ( 'crypto' ) ;
const bluebird = require ( 'bluebird' ) ;
2019-04-22 00:41:40 +00:00
const cryptoPseudoRandomBytes = bluebird . promisify ( crypto . pseudoRandomBytes . bind ( crypto ) ) ;
2018-08-02 10:19:27 +00:00
2018-08-03 11:35:55 +00:00
const entityTypes = entitySettings . getEntityTypes ( ) ;
2018-02-13 22:50:13 +00:00
const filesDir = path . join ( _ _dirname , '..' , 'files' ) ;
2018-08-03 11:35:55 +00:00
const ReplacementBehavior = entitySettings . ReplacementBehavior ;
2018-08-02 10:19:27 +00:00
function enforceTypePermitted ( type , subType ) {
2018-11-10 18:40:20 +00:00
enforce ( type in entityTypes && entityTypes [ type ] . files && entityTypes [ type ] . files [ subType ] , ` File type ${ type } : ${ subType } does not exist ` ) ;
2018-08-02 10:19:27 +00:00
}
function getFilePath ( type , subType , entityId , filename ) {
2018-12-26 03:38:02 +00:00
return path . join ( filesDir , type , subType , entityId . toString ( ) , filename ) ;
2018-04-02 17:05:22 +00:00
}
2018-02-13 22:50:13 +00:00
2018-08-02 10:19:27 +00:00
function getFileUrl ( context , type , subType , entityId , filename ) {
2018-12-15 14:15:48 +00:00
return getPublicUrl ( ` files/ ${ type } / ${ subType } / ${ entityId } / ${ filename } ` )
2018-02-13 22:50:13 +00:00
}
2018-08-02 10:19:27 +00:00
function getFilesTable ( type , subType ) {
return entityTypes [ type ] . files [ subType ] . table ;
2018-05-09 02:07:01 +00:00
}
2018-08-02 10:19:27 +00:00
function getFilesPermission ( type , subType , operation ) {
return entityTypes [ type ] . files [ subType ] . permissions [ operation ] ;
2018-02-13 22:50:13 +00:00
}
2018-08-02 10:19:27 +00:00
async function listDTAjax ( context , type , subType , entityId , params ) {
enforceTypePermitted ( type , subType ) ;
await shares . enforceEntityPermission ( context , type , entityId , getFilesPermission ( type , subType , 'view' ) ) ;
2018-02-13 22:50:13 +00:00
return await dtHelpers . ajaxList (
params ,
2019-06-25 05:18:06 +00:00
builder => builder . from ( getFilesTable ( type , subType ) ) . where ( { entity : entityId , delete _pending : false } ) ,
2018-03-24 22:55:50 +00:00
[ 'id' , 'originalname' , 'filename' , 'size' , 'created' ]
2018-02-13 22:50:13 +00:00
) ;
}
2018-09-18 08:30:13 +00:00
async function listTx ( tx , context , type , subType , entityId ) {
2018-08-02 10:19:27 +00:00
enforceTypePermitted ( type , subType ) ;
2018-09-18 08:30:13 +00:00
await shares . enforceEntityPermissionTx ( tx , context , type , entityId , getFilesPermission ( type , subType , 'view' ) ) ;
2019-06-25 05:18:06 +00:00
return await tx ( getFilesTable ( type , subType ) ) . where ( { entity : entityId , delete _pending : false } ) . select ( [ 'id' , 'originalname' , 'filename' , 'size' , 'created' ] ) . orderBy ( 'originalname' , 'asc' ) ;
2018-09-18 08:30:13 +00:00
}
async function list ( context , type , subType , entityId ) {
2018-03-24 22:55:50 +00:00
return await knex . transaction ( async tx => {
2018-11-10 18:40:20 +00:00
return await listTx ( tx , context , type , subType , entityId ) ;
2018-03-24 22:55:50 +00:00
} ) ;
}
2018-08-02 10:19:27 +00:00
async function getFileById ( context , type , subType , id ) {
enforceTypePermitted ( type , subType ) ;
2018-02-13 22:50:13 +00:00
const file = await knex . transaction ( async tx => {
2019-06-25 05:18:06 +00:00
const file = await tx ( getFilesTable ( type , subType ) ) . where ( { id : id , delete _pending : false } ) . first ( ) ;
2018-08-02 10:19:27 +00:00
await shares . enforceEntityPermissionTx ( tx , context , type , file . entity , getFilesPermission ( type , subType , 'view' ) ) ;
2018-02-13 22:50:13 +00:00
return file ;
} ) ;
2018-03-24 22:55:50 +00:00
if ( ! file ) {
throw new interoperableErrors . NotFoundError ( ) ;
}
2018-02-13 22:50:13 +00:00
return {
mimetype : file . mimetype ,
name : file . originalname ,
2018-08-02 10:19:27 +00:00
path : getFilePath ( type , subType , file . entity , file . filename )
2018-02-13 22:50:13 +00:00
} ;
}
2018-08-02 10:19:27 +00:00
async function _getFileBy ( context , type , subType , entityId , key , value ) {
enforceTypePermitted ( type , subType ) ;
2018-02-13 22:50:13 +00:00
const file = await knex . transaction ( async tx => {
2018-08-02 10:19:27 +00:00
await shares . enforceEntityPermissionTx ( tx , context , type , entityId , getFilesPermission ( type , subType , 'view' ) ) ;
2019-06-25 05:18:06 +00:00
const file = await tx ( getFilesTable ( type , subType ) ) . where ( { entity : entityId , delete _pending : false , [ key ] : value } ) . first ( ) ;
2018-02-13 22:50:13 +00:00
return file ;
} ) ;
2018-03-24 22:55:50 +00:00
if ( ! file ) {
throw new interoperableErrors . NotFoundError ( ) ;
}
2018-02-13 22:50:13 +00:00
return {
mimetype : file . mimetype ,
name : file . originalname ,
2018-08-02 10:19:27 +00:00
path : getFilePath ( type , subType , file . entity , file . filename )
2018-02-13 22:50:13 +00:00
} ;
}
2018-08-02 10:19:27 +00:00
async function getFileByOriginalName ( context , type , subType , entityId , name ) {
return await _getFileBy ( context , type , subType , entityId , 'originalname' , name )
2018-05-13 20:40:34 +00:00
}
2018-08-02 10:19:27 +00:00
async function getFileByFilename ( context , type , subType , entityId , name ) {
return await _getFileBy ( context , type , subType , entityId , 'filename' , name )
2018-05-13 20:40:34 +00:00
}
2018-08-03 11:35:55 +00:00
async function getFileByUrl ( context , url ) {
2018-12-15 14:15:48 +00:00
const urlPrefix = getPublicUrl ( 'files/' ) ;
2018-05-09 02:07:01 +00:00
if ( url . startsWith ( urlPrefix ) ) {
2018-08-03 11:35:55 +00:00
const path = url . substring ( urlPrefix . length ) ;
const pathElem = path . split ( '/' ) ;
if ( pathElem . length !== 4 ) {
throw new interoperableErrors . NotFoundError ( ) ;
}
const type = pathElem [ 0 ] ;
const subType = pathElem [ 1 ] ;
const entityId = Number . parseInt ( pathElem [ 2 ] ) ;
if ( Number . isNaN ( entityId ) ) {
throw new interoperableErrors . NotFoundError ( ) ;
}
const name = pathElem [ 3 ] ;
2018-08-02 10:19:27 +00:00
return await getFileByFilename ( context , type , subType , entityId , name ) ;
2018-05-09 02:07:01 +00:00
} else {
throw new interoperableErrors . NotFoundError ( ) ;
}
}
2018-08-02 10:19:27 +00:00
// Adds files to an entity. The source data can be either a file (then it's path is contained in file.path) or in-memory data (then it's content is in file.data).
2018-11-10 18:40:20 +00:00
async function createFiles ( context , type , subType , entityId , files , replacementBehavior , transformResponseFn ) {
2018-08-02 10:19:27 +00:00
enforceTypePermitted ( type , subType ) ;
2018-02-13 22:50:13 +00:00
if ( files . length == 0 ) {
// No files uploaded
return { uploaded : 0 } ;
}
2018-08-03 11:35:55 +00:00
if ( ! replacementBehavior ) {
replacementBehavior = entityTypes [ type ] . files [ subType ] . defaultReplacementBehavior ;
}
2018-03-24 22:55:50 +00:00
const fileEntities = [ ] ;
const filesToMove = [ ] ;
const ignoredFiles = [ ] ;
const removedFiles = [ ] ;
const filesRet = [ ] ;
await knex . transaction ( async tx => {
2018-08-02 10:19:27 +00:00
await shares . enforceEntityPermissionTx ( tx , context , type , entityId , getFilesPermission ( type , subType , 'manage' ) ) ;
2018-03-24 22:55:50 +00:00
2019-06-25 05:18:06 +00:00
const existingNamesRows = await tx ( getFilesTable ( type , subType ) ) . where ( { entity : entityId , delete _pending : false } ) . select ( [ 'id' , 'filename' , 'originalname' ] ) ;
2018-08-02 10:19:27 +00:00
const existingNameSet = new Set ( ) ;
2018-03-24 22:55:50 +00:00
for ( const row of existingNamesRows ) {
2018-08-02 10:19:27 +00:00
existingNameSet . add ( row . originalname ) ;
2018-02-13 22:50:13 +00:00
}
2018-08-02 10:19:27 +00:00
// The processedNameSet holds originalnames of entries which have been already processed in the upload batch. It prevents uploading two files with the same originalname
const processedNameSet = new Set ( ) ;
2018-03-24 22:55:50 +00:00
// Create entities for files
for ( const file of files ) {
const parsedOriginalName = path . parse ( file . originalname ) ;
let originalName = parsedOriginalName . base ;
2018-08-02 10:19:27 +00:00
if ( ! file . filename ) {
// This is taken from multer/storage/disk.js and adapted for async/await
file . filename = ( await cryptoPseudoRandomBytes ( 16 ) ) . toString ( 'hex' ) ;
}
if ( replacementBehavior === ReplacementBehavior . RENAME ) {
2018-03-24 22:55:50 +00:00
let suffix = 1 ;
2018-08-02 10:19:27 +00:00
while ( existingNameSet . has ( originalName ) || processedNameSet . has ( originalName ) ) {
2018-03-24 22:55:50 +00:00
originalName = parsedOriginalName . name + '-' + suffix + parsedOriginalName . ext ;
suffix ++ ;
}
}
2018-08-02 10:19:27 +00:00
if ( replacementBehavior === ReplacementBehavior . NONE && ( existingNameSet . has ( originalName ) || processedNameSet . has ( originalName ) ) ) {
// The file has an original name same as another file in the same upload batch or it has an original name same as another already existing file
2018-03-24 22:55:50 +00:00
ignoredFiles . push ( file ) ;
} else {
filesToMove . push ( file ) ;
fileEntities . push ( {
entity : entityId ,
filename : file . filename ,
originalname : originalName ,
mimetype : file . mimetype ,
size : file . size
} ) ;
2018-05-09 02:07:01 +00:00
const filesRetEntry = {
2018-03-24 22:55:50 +00:00
name : file . filename ,
originalName : originalName ,
size : file . size ,
2018-08-02 10:19:27 +00:00
type : file . mimetype
2018-05-09 02:07:01 +00:00
} ;
2018-08-02 10:19:27 +00:00
filesRetEntry . url = getFileUrl ( context , type , subType , entityId , file . filename ) ;
if ( file . mimetype . startsWith ( 'image/' ) ) {
filesRetEntry . thumbnailUrl = getFileUrl ( context , type , subType , entityId , file . filename ) ; // TODO - use smaller thumbnails,
}
2018-05-09 02:07:01 +00:00
filesRet . push ( filesRetEntry ) ;
2018-08-02 10:19:27 +00:00
}
processedNameSet . add ( originalName ) ;
}
2018-03-24 22:55:50 +00:00
2018-08-02 10:19:27 +00:00
if ( replacementBehavior === ReplacementBehavior . REPLACE ) {
2018-08-02 11:35:57 +00:00
const idsToRemove = [ ] ;
2018-08-02 10:19:27 +00:00
for ( const row of existingNamesRows ) {
if ( processedNameSet . has ( row . originalname ) ) {
removedFiles . push ( row ) ;
idsToRemove . push ( row . id ) ;
2018-03-24 22:55:50 +00:00
}
}
2018-08-02 10:19:27 +00:00
await tx ( getFilesTable ( type , subType ) ) . where ( 'entity' , entityId ) . whereIn ( 'id' , idsToRemove ) . del ( ) ;
2018-03-24 22:55:50 +00:00
}
2018-02-13 22:50:13 +00:00
2018-03-24 22:55:50 +00:00
if ( fileEntities ) {
2018-08-02 10:19:27 +00:00
await tx ( getFilesTable ( type , subType ) ) . insert ( fileEntities ) ;
2018-02-13 22:50:13 +00:00
}
} ) ;
// Move new files from upload directory to files directory
2018-03-24 22:55:50 +00:00
for ( const file of filesToMove ) {
2018-08-02 10:19:27 +00:00
const filePath = getFilePath ( type , subType , entityId , file . filename ) ;
if ( file . path ) {
// The names should be unique, so overwrite is disabled
// The directory is created if it does not exist
// Empty options argument is passed, otherwise fails
await fs . moveAsync ( file . path , filePath , { } ) ;
} else if ( file . data ) {
await fs . outputFile ( filePath , file . data ) ;
}
2018-02-13 22:50:13 +00:00
}
// Remove replaced files from files directory
2018-03-24 22:55:50 +00:00
for ( const file of removedFiles ) {
2018-08-02 10:19:27 +00:00
const filePath = getFilePath ( type , subType , entityId , file . filename ) ;
2018-05-09 02:07:01 +00:00
await fs . removeAsync ( filePath ) ;
2018-02-13 22:50:13 +00:00
}
// Remove ignored files from upload directory
2018-03-24 22:55:50 +00:00
for ( const file of ignoredFiles ) {
2018-08-02 10:19:27 +00:00
if ( file . path ) {
await fs . removeAsync ( file . path ) ;
}
2018-02-13 22:50:13 +00:00
}
2018-11-10 18:40:20 +00:00
const resp = {
2018-02-13 22:50:13 +00:00
uploaded : files . length ,
2018-03-24 22:55:50 +00:00
added : fileEntities . length - removedFiles . length ,
2018-02-13 22:50:13 +00:00
replaced : removedFiles . length ,
2018-03-24 22:55:50 +00:00
ignored : ignoredFiles . length ,
files : filesRet
2018-02-13 22:50:13 +00:00
} ;
2018-11-10 18:40:20 +00:00
if ( transformResponseFn ) {
return transformResponseFn ( resp ) ;
} else {
return resp ;
}
2018-02-13 22:50:13 +00:00
}
2019-06-25 05:18:06 +00:00
async function lockTx ( tx , type , subType , id ) {
enforceTypePermitted ( type , subType ) ;
const filesTableName = getFilesTable ( type , subType ) ;
await tx ( filesTableName ) . where ( 'id' , id ) . increment ( 'lock_count' ) ;
}
async function unlockTx ( tx , type , subType , id ) {
enforceTypePermitted ( type , subType ) ;
const filesTableName = getFilesTable ( type , subType ) ;
const file = await tx ( filesTableName ) . where ( 'id' , id ) . first ( ) ;
enforce ( file , ` File ${ id } not found ` ) ;
enforce ( file . lock _count > 0 , ` Corrupted lock count at file ${ id } ` ) ;
if ( file . lock _count === 1 && file . delete _pending ) {
await tx ( filesTableName ) . where ( 'id' , id ) . del ( ) ;
const filePath = getFilePath ( type , subType , file . entity , file . filename ) ;
await fs . removeAsync ( filePath ) ;
} else {
await tx ( filesTableName ) . where ( 'id' , id ) . update ( { lock _count : file . lock _count - 1 } ) ;
}
}
2018-08-02 10:19:27 +00:00
async function removeFile ( context , type , subType , id ) {
enforceTypePermitted ( type , subType ) ;
2018-07-31 04:34:28 +00:00
2019-06-25 05:18:06 +00:00
await knex . transaction ( async tx => {
const filesTableName = getFilesTable ( type , subType ) ;
const file = await tx ( filesTableName ) . where ( 'id' , id ) . first ( ) ;
2018-08-02 10:19:27 +00:00
await shares . enforceEntityPermissionTx ( tx , context , type , file . entity , getFilesPermission ( type , subType , 'manage' ) ) ;
2018-02-13 22:50:13 +00:00
2019-06-25 05:18:06 +00:00
if ( ! file . lock _count ) {
await tx ( filesTableName ) . where ( 'id' , file . id ) . del ( ) ;
const filePath = getFilePath ( type , subType , file . entity , file . filename ) ;
await fs . removeAsync ( filePath ) ;
} else {
await tx ( filesTableName ) . where ( 'id' , file . id ) . update ( { delete _pending : true } ) ;
}
} ) ;
2018-02-13 22:50:13 +00:00
}
2018-08-02 10:19:27 +00:00
async function copyAllTx ( tx , context , fromType , fromSubType , fromEntityId , toType , toSubType , toEntityId ) {
enforceTypePermitted ( fromType , fromSubType ) ;
await shares . enforceEntityPermissionTx ( tx , context , fromType , fromEntityId , getFilesPermission ( fromType , fromSubType , 'view' ) ) ;
2018-07-31 04:34:28 +00:00
2018-08-02 10:19:27 +00:00
enforceTypePermitted ( toType , toSubType ) ;
await shares . enforceEntityPermissionTx ( tx , context , toType , toEntityId , getFilesPermission ( toType , toSubType , 'manage' ) ) ;
2018-07-31 04:34:28 +00:00
2019-06-25 05:18:06 +00:00
const rows = await tx ( getFilesTable ( fromType , fromSubType ) ) . where ( { entity : fromEntityId , delete _pending : false } ) ;
2018-07-31 04:34:28 +00:00
for ( const row of rows ) {
2018-08-02 10:19:27 +00:00
const fromFilePath = getFilePath ( fromType , fromSubType , fromEntityId , row . filename ) ;
const toFilePath = getFilePath ( toType , toSubType , toEntityId , row . filename ) ;
2018-07-31 04:34:28 +00:00
await fs . copyAsync ( fromFilePath , toFilePath , { } ) ;
delete row . id ;
row . entity = toEntityId ;
}
2018-08-03 11:35:55 +00:00
if ( rows . length > 0 ) {
await tx ( getFilesTable ( toType , toSubType ) ) . insert ( rows ) ;
}
2018-07-31 04:34:28 +00:00
}
2018-09-29 11:30:29 +00:00
async function removeAllTx ( tx , context , type , subType , entityId ) {
enforceTypePermitted ( type , subType ) ;
await shares . enforceEntityPermissionTx ( tx , context , type , entityId , getFilesPermission ( type , subType , 'manage' ) ) ;
const rows = await tx ( getFilesTable ( type , subType ) ) . where ( { entity : entityId } ) ;
for ( const row of rows ) {
const filePath = getFilePath ( type , subType , entityId , row . filename ) ;
await fs . removeAsync ( filePath ) ;
}
await tx ( getFilesTable ( type , subType ) ) . where ( 'entity' , entityId ) . del ( ) ;
}
2018-07-31 04:34:28 +00:00
2018-09-09 22:55:44 +00:00
module . exports . filesDir = filesDir ;
module . exports . listDTAjax = listDTAjax ;
2018-09-18 08:30:13 +00:00
module . exports . listTx = listTx ;
2018-09-09 22:55:44 +00:00
module . exports . list = list ;
module . exports . getFileById = getFileById ;
module . exports . getFileByFilename = getFileByFilename ;
module . exports . getFileByUrl = getFileByUrl ;
module . exports . getFileByOriginalName = getFileByOriginalName ;
module . exports . createFiles = createFiles ;
module . exports . removeFile = removeFile ;
module . exports . getFileUrl = getFileUrl ;
module . exports . getFilePath = getFilePath ;
module . exports . copyAllTx = copyAllTx ;
2018-09-29 11:30:29 +00:00
module . exports . removeAllTx = removeAllTx ;
2019-06-25 05:18:06 +00:00
module . exports . lockTx = lockTx ;
module . exports . unlockTx = unlockTx ;
2018-09-09 22:55:44 +00:00
module . exports . ReplacementBehavior = ReplacementBehavior ;