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-03-24 22:55:50 +00:00
const interoperableErrors = require ( '../shared/interoperable-errors' ) ;
2018-04-02 17:05:22 +00:00
const permissions = require ( '../lib/permissions' ) ;
2018-05-13 20:40:34 +00:00
const { getTrustedUrl } = 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' ) ;
const cryptoPseudoRandomBytes = bluebird . promisify ( crypto . pseudoRandomBytes ) ;
2018-04-02 17:05:22 +00:00
const entityTypes = permissions . getEntityTypes ( ) ;
2018-02-13 22:50:13 +00:00
const filesDir = path . join ( _ _dirname , '..' , 'files' ) ;
2018-08-02 10:19:27 +00:00
const ReplacementBehavior = {
NONE : 0 ,
REPLACE : 1 ,
RENAME : 2
} ;
function enforceTypePermitted ( type , subType ) {
enforce ( type in entityTypes && entityTypes [ type ] . files && entityTypes [ type ] . files [ subType ] ) ;
}
function getFilePath ( type , subType , entityId , filename ) {
return path . join ( 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 ) {
return getTrustedUrl ( ` files/ ${ type } / ${ subType } / ${ entityId } / ${ filename } ` , context )
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 ,
2018-08-02 10:19:27 +00:00
builder => builder . from ( getFilesTable ( type , subType ) ) . where ( { entity : entityId } ) ,
2018-03-24 22:55:50 +00:00
[ 'id' , 'originalname' , 'filename' , 'size' , 'created' ]
2018-02-13 22:50:13 +00:00
) ;
}
2018-08-02 10:19:27 +00:00
async function list ( context , type , subType , entityId ) {
enforceTypePermitted ( type , subType ) ;
2018-03-24 22:55:50 +00:00
return await knex . transaction ( async tx => {
2018-08-02 10:19:27 +00:00
await shares . enforceEntityPermissionTx ( tx , context , type , entityId , getFilesPermission ( type , subType , 'view' ) ) ;
return await tx ( getFilesTable ( type , subType ) ) . where ( { entity : entityId } ) . select ( [ 'id' , 'originalname' , 'filename' , 'size' , 'created' ] ) . orderBy ( 'originalname' , 'asc' ) ;
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 => {
2018-08-02 10:19:27 +00:00
const file = await tx ( getFilesTable ( type , subType ) ) . where ( 'id' , id ) . first ( ) ;
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' ) ) ;
const file = await tx ( getFilesTable ( type , subType ) ) . where ( { entity : entityId , [ 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-02 10:19:27 +00:00
async function getFileByUrl ( context , type , subType , entityId , url ) {
const urlPrefix = getTrustedUrl ( ` files/ ${ type } / ${ subType } / ${ entityId } / ` , context ) ;
2018-05-09 02:07:01 +00:00
if ( url . startsWith ( urlPrefix ) ) {
const name = url . substring ( urlPrefix . length ) ;
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).
async function createFiles ( context , type , subType , entityId , files , replacementBehavior ) {
enforceTypePermitted ( type , subType ) ;
2018-02-13 22:50:13 +00:00
if ( files . length == 0 ) {
// No files uploaded
return { uploaded : 0 } ;
}
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
2018-08-02 10:19:27 +00:00
const existingNamesRows = await tx ( getFilesTable ( type , subType ) ) . where ( 'entity' , entityId ) . select ( [ 'id' , 'filename' , 'originalname' ] ) ;
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 ,
encoding : file . encoding ,
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 ) {
for ( const row of existingNamesRows ) {
const idsToRemove = [ ] ;
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
}
return {
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-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
2018-02-13 22:50:13 +00:00
const file = await knex . transaction ( async tx => {
2018-08-02 10:19:27 +00:00
const file = await tx ( getFilesTable ( type , subType ) ) . where ( 'id' , id ) . select ( 'entity' , 'filename' ) . first ( ) ;
await shares . enforceEntityPermissionTx ( tx , context , type , file . entity , getFilesPermission ( type , subType , 'manage' ) ) ;
await tx ( getFilesTable ( type , subType ) ) . where ( 'id' , id ) . del ( ) ;
2018-02-13 22:50:13 +00:00
return { filename : file . filename , entity : file . entity } ;
} ) ;
2018-08-02 10:19:27 +00:00
const filePath = getFilePath ( type , subType , file . entity , file . filename ) ;
2018-05-09 02:07:01 +00:00
await fs . removeAsync ( filePath ) ;
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
2018-08-02 10:19:27 +00:00
const rows = await tx ( getFilesTable ( fromType , fromSubType ) ) . where ( { entity : fromEntityId } ) ;
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-02 10:19:27 +00:00
await tx ( getFilesTable ( toType , toSubType ) ) . insert ( rows ) ;
2018-07-31 04:34:28 +00:00
}
2018-02-13 22:50:13 +00:00
module . exports = {
2018-05-09 02:07:01 +00:00
filesDir ,
2018-03-24 22:55:50 +00:00
listDTAjax ,
list ,
2018-02-13 22:50:13 +00:00
getFileById ,
2018-03-24 22:55:50 +00:00
getFileByFilename ,
2018-05-09 02:07:01 +00:00
getFileByUrl ,
2018-05-13 20:40:34 +00:00
getFileByOriginalName ,
2018-02-13 22:50:13 +00:00
createFiles ,
2018-03-24 22:55:50 +00:00
removeFile ,
2018-05-09 02:07:01 +00:00
getFileUrl ,
2018-07-31 04:34:28 +00:00
getFilePath ,
2018-08-02 10:19:27 +00:00
copyAllTx ,
ReplacementBehavior
2018-02-13 22:50:13 +00:00
} ;