2021-11-18 18:53:00 -06:00
const axios = require ( 'axios' )
2022-04-26 19:11:32 -05:00
const Path = require ( 'path' )
2023-10-13 16:33:47 -05:00
const ssrfFilter = require ( 'ssrf-req-filter' )
2024-01-03 16:23:17 -06:00
const exec = require ( 'child_process' ) . exec
2023-10-13 16:33:47 -05:00
const fs = require ( '../libs/fsExtra' )
const rra = require ( '../libs/recursiveReaddirAsync' )
2021-09-29 20:43:36 -05:00
const Logger = require ( '../Logger' )
2022-07-24 13:32:05 -05:00
const { AudioMimeType } = require ( './constants' )
2021-08-17 17:01:11 -05:00
2023-01-06 17:10:55 -06:00
/ * *
2024-06-03 17:21:18 -05:00
* Make sure folder separator is POSIX for Windows file paths . e . g . "C:\Users\Abs" becomes "C:/Users/Abs"
*
* @ param { String } path - Ugly file path
* @ return { String } Pretty posix file path
* /
2023-01-06 17:10:55 -06:00
const filePathToPOSIX = ( path ) => {
if ( ! global . isWin || ! path ) return path
2024-08-07 21:18:53 +03:00
return path . startsWith ( '\\\\' ) ? '\\\\' + path . slice ( 2 ) . replace ( /\\/g , '/' ) : path . replace ( /\\/g , '/' )
2023-01-06 17:10:55 -06:00
}
module . exports . filePathToPOSIX = filePathToPOSIX
2023-10-23 17:28:59 -05:00
/ * *
* Check path is a child of or equal to another path
2024-06-03 17:21:18 -05:00
*
* @ param { string } parentPath
* @ param { string } childPath
2023-10-23 17:28:59 -05:00
* @ returns { boolean }
* /
2023-10-23 21:48:34 +00:00
function isSameOrSubPath ( parentPath , childPath ) {
parentPath = filePathToPOSIX ( parentPath )
childPath = filePathToPOSIX ( childPath )
if ( parentPath === childPath ) return true
const relativePath = Path . relative ( parentPath , childPath )
return (
2024-06-03 17:21:18 -05:00
relativePath === '' || // Same path (e.g. parentPath = '/a/b/', childPath = '/a/b')
( ! relativePath . startsWith ( '..' ) && ! Path . isAbsolute ( relativePath ) ) // Sub path
2023-10-23 21:48:34 +00:00
)
}
module . exports . isSameOrSubPath = isSameOrSubPath
2023-10-25 16:53:53 -05:00
function getFileStat ( path ) {
2021-08-17 17:01:11 -05:00
try {
2023-10-25 16:53:53 -05:00
return fs . stat ( path )
2021-08-17 17:01:11 -05:00
} catch ( err ) {
2022-08-15 17:35:13 -05:00
Logger . error ( '[fileUtils] Failed to stat' , err )
2023-10-25 16:53:53 -05:00
return null
2021-08-17 17:01:11 -05:00
}
}
2022-02-27 18:07:36 -06:00
async function getFileTimestampsWithIno ( path ) {
try {
var stat = await fs . stat ( path , { bigint : true } )
return {
size : Number ( stat . size ) ,
mtimeMs : Number ( stat . mtimeMs ) ,
ctimeMs : Number ( stat . ctimeMs ) ,
birthtimeMs : Number ( stat . birthtimeMs ) ,
ino : String ( stat . ino )
}
} catch ( err ) {
2024-04-17 17:09:36 -05:00
Logger . error ( ` [fileUtils] Failed to getFileTimestampsWithIno for path " ${ path } " ` , err )
2022-02-27 18:07:36 -06:00
return false
}
}
module . exports . getFileTimestampsWithIno = getFileTimestampsWithIno
2023-10-25 16:53:53 -05:00
/ * *
* Get file size
2024-06-03 17:21:18 -05:00
*
* @ param { string } path
2023-10-25 16:53:53 -05:00
* @ returns { Promise < number > }
* /
module . exports . getFileSize = async ( path ) => {
return ( await getFileStat ( path ) ) ? . size || 0
}
/ * *
* Get file mtimeMs
2024-06-03 17:21:18 -05:00
*
* @ param { string } path
2023-10-25 16:53:53 -05:00
* @ returns { Promise < number > } epoch timestamp
* /
module . exports . getFileMTimeMs = async ( path ) => {
2023-12-29 10:04:59 -06:00
try {
return ( await getFileStat ( path ) ) ? . mtimeMs || 0
} catch ( err ) {
Logger . error ( ` [fileUtils] Failed to getFileMtimeMs ` , err )
return 0
}
2021-09-04 14:17:26 -05:00
}
2023-08-30 18:05:52 -05:00
/ * *
2024-06-03 17:21:18 -05:00
*
* @ param { string } filepath
2023-08-30 18:05:52 -05:00
* @ returns { boolean }
* /
async function checkPathIsFile ( filepath ) {
try {
const stat = await fs . stat ( filepath )
return stat . isFile ( )
} catch ( err ) {
return false
}
}
module . exports . checkPathIsFile = checkPathIsFile
2022-02-27 18:07:36 -06:00
function getIno ( path ) {
2024-06-03 17:21:18 -05:00
return fs
. stat ( path , { bigint : true } )
. then ( ( data ) => String ( data . ino ) )
. catch ( ( err ) => {
Logger . error ( '[Utils] Failed to get ino for path' , path , err )
return null
} )
2022-02-27 18:07:36 -06:00
}
module . exports . getIno = getIno
2023-09-01 18:01:17 -05:00
/ * *
* Read contents of file
2024-06-03 17:21:18 -05:00
* @ param { string } path
2023-09-01 18:01:17 -05:00
* @ returns { string }
* /
2021-09-29 20:43:36 -05:00
async function readTextFile ( path ) {
try {
var data = await fs . readFile ( path )
return String ( data )
} catch ( error ) {
Logger . error ( ` [FileUtils] ReadTextFile error ${ error } ` )
return ''
}
}
module . exports . readTextFile = readTextFile
2025-02-22 12:28:51 -07:00
/ * *
* Check if file or directory should be ignored . Returns a string of the reason to ignore , or null if not ignored
*
* @ param { string } path
* @ returns { string }
* /
module . exports . shouldIgnoreFile = ( path ) => {
// Check if directory or file name starts with "."
if ( Path . basename ( path ) . startsWith ( '.' ) ) {
return 'dotfile'
}
if ( path . split ( '/' ) . find ( ( p ) => p . startsWith ( '.' ) ) ) {
return 'dotpath'
}
// If these strings exist anywhere in the filename or directory name, ignore. Vendor specific hidden directories
2025-02-23 16:53:11 -06:00
const includeAnywhereIgnore = [ '@eaDir' ]
const filteredInclude = includeAnywhereIgnore . filter ( ( str ) => path . includes ( str ) )
2025-02-22 12:28:51 -07:00
if ( filteredInclude . length ) {
return ` ${ filteredInclude [ 0 ] } directory `
}
2025-02-23 16:53:11 -06:00
const extensionIgnores = [ '.part' , '.tmp' , '.crdownload' , '.download' , '.bak' , '.old' , '.temp' , '.tempfile' , '.tempfile~' ]
// Check extension
if ( extensionIgnores . includes ( Path . extname ( path ) . toLowerCase ( ) ) ) {
// Return the extension that is ignored
return ` ${ Path . extname ( path ) } file `
}
2025-02-22 12:28:51 -07:00
// Should not ignore this file or directory
return null
}
2024-12-04 16:25:17 -06:00
/ * *
* @ typedef FilePathItem
* @ property { string } name - file name e . g . "audiofile.m4b"
* @ property { string } path - fullpath excluding folder e . g . "Author/Book/audiofile.m4b"
* @ property { string } reldirpath - path excluding file name e . g . "Author/Book"
* @ property { string } fullpath - full path e . g . "/audiobooks/Author/Book/audiofile.m4b"
* @ property { string } extension - file extension e . g . ".m4b"
* @ property { number } deep - depth of file in directory ( 0 is file in folder root )
* /
2023-08-26 16:33:27 -05:00
/ * *
* Get array of files inside dir
2024-06-03 17:21:18 -05:00
* @ param { string } path
* @ param { string } [ relPathToReplace ]
2024-12-04 16:25:17 -06:00
* @ returns { FilePathItem [ ] }
2023-08-26 16:33:27 -05:00
* /
2025-02-23 16:53:11 -06:00
module . exports . recurseFiles = async ( path , relPathToReplace = null ) => {
2023-01-06 17:10:55 -06:00
path = filePathToPOSIX ( path )
2021-11-06 17:26:44 -05:00
if ( ! path . endsWith ( '/' ) ) path = path + '/'
2021-11-21 20:00:40 -06:00
if ( relPathToReplace ) {
2023-01-06 17:10:55 -06:00
relPathToReplace = filePathToPOSIX ( relPathToReplace )
2021-11-21 20:00:40 -06:00
if ( ! relPathToReplace . endsWith ( '/' ) ) relPathToReplace += '/'
} else {
relPathToReplace = path
}
2021-11-06 17:26:44 -05:00
const options = {
mode : rra . LIST ,
recursive : true ,
stats : false ,
ignoreFolders : true ,
extensions : true ,
deep : true ,
realPath : true ,
2024-08-07 21:18:53 +03:00
normalizePath : false
2021-11-06 17:26:44 -05:00
}
2023-09-08 14:50:59 -05:00
let list = await rra . list ( path , options )
2021-11-06 17:26:44 -05:00
if ( list . error ) {
Logger . error ( '[fileUtils] Recurse files error' , list . error )
return [ ]
}
2022-04-26 19:11:32 -05:00
const directoriesToIgnore = [ ]
2024-06-03 17:21:18 -05:00
list = list
. filter ( ( item ) => {
if ( item . error ) {
Logger . error ( ` [fileUtils] Recurse files file " ${ item . fullname } " has error ` , item . error )
return false
}
2021-11-06 17:26:44 -05:00
2024-08-07 21:18:53 +03:00
item . fullname = filePathToPOSIX ( item . fullname )
item . path = filePathToPOSIX ( item . path )
2024-06-03 17:21:18 -05:00
const relpath = item . fullname . replace ( relPathToReplace , '' )
let reldirname = Path . dirname ( relpath )
if ( reldirname === '.' ) reldirname = ''
const dirname = Path . dirname ( item . fullname )
2022-04-26 19:11:32 -05:00
2024-06-03 17:21:18 -05:00
// Directory has a file named ".ignore" flag directory and ignore
if ( item . name === '.ignore' && reldirname && reldirname !== '.' && ! directoriesToIgnore . includes ( dirname ) ) {
Logger . debug ( ` [fileUtils] .ignore found - ignoring directory " ${ reldirname } " ` )
directoriesToIgnore . push ( dirname )
return false
}
2022-04-26 19:11:32 -05:00
2025-02-22 12:28:51 -07:00
// Check for ignored extensions or directories
const shouldIgnore = this . shouldIgnoreFile ( relpath )
if ( shouldIgnore ) {
Logger . debug ( ` [fileUtils] Ignoring ${ shouldIgnore } - " ${ relpath } " ` )
2024-06-03 17:21:18 -05:00
return false
}
2021-11-06 17:26:44 -05:00
2024-06-03 17:21:18 -05:00
return true
} )
. filter ( ( item ) => {
// Filter out items in ignore directories
if ( directoriesToIgnore . some ( ( dir ) => item . fullname . startsWith ( dir ) ) ) {
Logger . debug ( ` [fileUtils] Ignoring path in dir with .ignore " ${ item . fullname } " ` )
return false
}
return true
} )
. map ( ( item ) => {
var isInRoot = item . path + '/' === relPathToReplace
return {
name : item . name ,
path : item . fullname . replace ( relPathToReplace , '' ) ,
reldirpath : isInRoot ? '' : item . path . replace ( relPathToReplace , '' ) ,
fullpath : item . fullname ,
extension : item . extension ,
deep : item . deep
}
} )
2021-11-06 17:26:44 -05:00
// Sort from least deep to most
list . sort ( ( a , b ) => a . deep - b . deep )
return list
}
2021-11-17 19:19:24 -06:00
2024-12-04 16:25:17 -06:00
/ * *
*
* @ param { import ( '../Watcher' ) . PendingFileUpdate } fileUpdate
* @ returns { FilePathItem }
* /
module . exports . getFilePathItemFromFileUpdate = ( fileUpdate ) => {
let relPath = fileUpdate . relPath
if ( relPath . startsWith ( '/' ) ) relPath = relPath . slice ( 1 )
const dirname = Path . dirname ( relPath )
return {
name : Path . basename ( relPath ) ,
path : relPath ,
reldirpath : dirname === '.' ? '' : dirname ,
fullpath : fileUpdate . path ,
extension : Path . extname ( relPath ) ,
deep : relPath . split ( '/' ) . length - 1
}
}
2023-10-14 10:52:56 -05:00
/ * *
* Download file from web to local file system
* Uses SSRF filter to prevent internal URLs
2024-06-03 17:21:18 -05:00
*
* @ param { string } url
2023-10-14 10:52:56 -05:00
* @ param { string } filepath path to download the file to
* @ param { Function } [ contentTypeFilter ] validate content type before writing
* @ returns { Promise }
* /
module . exports . downloadFile = ( url , filepath , contentTypeFilter = null ) => {
2023-04-26 18:15:50 -05:00
return new Promise ( async ( resolve , reject ) => {
Logger . debug ( ` [fileUtils] Downloading file to ${ filepath } ` )
axios ( {
url ,
method : 'GET' ,
responseType : 'stream' ,
2024-06-24 17:14:20 -05:00
headers : {
'User-Agent' : 'audiobookshelf (+https://audiobookshelf.org)'
} ,
2023-10-13 16:33:47 -05:00
timeout : 30000 ,
2024-12-24 15:07:11 -06:00
httpAgent : global . DisableSsrfRequestFilter ? . ( url ) ? null : ssrfFilter ( url ) ,
httpsAgent : global . DisableSsrfRequestFilter ? . ( url ) ? null : ssrfFilter ( url )
2024-06-03 17:21:18 -05:00
} )
. then ( ( response ) => {
// Validate content type
if ( contentTypeFilter && ! contentTypeFilter ? . ( response . headers ? . [ 'content-type' ] ) ) {
return reject ( new Error ( ` Invalid content type " ${ response . headers ? . [ 'content-type' ] || '' } " ` ) )
}
2023-10-14 10:52:56 -05:00
2025-02-05 16:15:00 -06:00
const totalSize = parseInt ( response . headers [ 'content-length' ] , 10 )
let downloadedSize = 0
2024-06-03 17:21:18 -05:00
// Write to filepath
const writer = fs . createWriteStream ( filepath )
response . data . pipe ( writer )
2023-04-26 18:15:50 -05:00
2025-02-05 16:15:00 -06:00
let lastProgress = 0
response . data . on ( 'data' , ( chunk ) => {
downloadedSize += chunk . length
const progress = totalSize ? Math . round ( ( downloadedSize / totalSize ) * 100 ) : 0
if ( progress >= lastProgress + 5 ) {
Logger . debug ( ` [fileUtils] File " ${ Path . basename ( filepath ) } " download progress: ${ progress } % ( ${ downloadedSize } / ${ totalSize } bytes) ` )
lastProgress = progress
}
} )
2024-06-03 17:21:18 -05:00
writer . on ( 'finish' , resolve )
writer . on ( 'error' , reject )
} )
. catch ( ( err ) => {
Logger . error ( ` [fileUtils] Failed to download file " ${ filepath } " ` , err )
reject ( err )
} )
2021-11-17 19:19:24 -06:00
} )
2022-03-21 19:24:38 -05:00
}
2023-10-14 10:52:56 -05:00
/ * *
* Download image file from web to local file system
* Response header must have content - type of image / ( excluding svg )
2024-06-03 17:21:18 -05:00
*
* @ param { string } url
* @ param { string } filepath
2023-10-14 10:52:56 -05:00
* @ returns { Promise }
* /
module . exports . downloadImageFile = ( url , filepath ) => {
const contentTypeFilter = ( contentType ) => {
return contentType ? . startsWith ( 'image/' ) && contentType !== 'image/svg+xml'
}
return this . downloadFile ( url , filepath , contentTypeFilter )
}
2022-05-13 17:13:58 -05:00
module . exports . sanitizeFilename = ( filename , colonReplacement = ' - ' ) => {
2022-03-21 19:24:38 -05:00
if ( typeof filename !== 'string' ) {
return false
}
2022-05-13 17:13:58 -05:00
2025-03-19 17:39:23 -05:00
// Normalize the string first to ensure consistent byte calculations
filename = filename . normalize ( 'NFC' )
2022-12-13 17:46:18 -06:00
// Most file systems use number of bytes for max filename
// to support most filesystems we will use max of 255 bytes in utf-16
// Ref: https://doc.owncloud.com/server/next/admin_manual/troubleshooting/path_filename_length.html
// Issue: https://github.com/advplyr/audiobookshelf/issues/1261
const MAX _FILENAME _BYTES = 255
const replacement = ''
const illegalRe = /[\/\?<>\\:\*\|"]/g
const controlRe = /[\x00-\x1f\x80-\x9f]/g
const reservedRe = /^\.+$/
const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i
const windowsTrailingRe = /[\. ]+$/
const lineBreaks = /[\n\r]/g
2022-03-21 19:24:38 -05:00
2022-12-19 15:02:31 -06:00
let sanitized = filename
2022-05-13 17:13:58 -05:00
. replace ( ':' , colonReplacement ) // Replace first occurrence of a colon
2022-03-21 19:24:38 -05:00
. replace ( illegalRe , replacement )
. replace ( controlRe , replacement )
. replace ( reservedRe , replacement )
2022-06-01 20:14:10 -05:00
. replace ( lineBreaks , replacement )
2022-03-21 19:24:38 -05:00
. replace ( windowsReservedRe , replacement )
2022-06-01 20:14:10 -05:00
. replace ( windowsTrailingRe , replacement )
2023-12-01 21:42:54 -08:00
. replace ( /\s+/g , ' ' ) // Replace consecutive spaces with a single space
2022-06-01 20:14:10 -05:00
2022-12-13 17:46:18 -06:00
// Check if basename is too many bytes
const ext = Path . extname ( sanitized ) // separate out file extension
const basename = Path . basename ( sanitized , ext )
const extByteLength = Buffer . byteLength ( ext , 'utf16le' )
2025-03-19 17:39:23 -05:00
2022-12-13 17:46:18 -06:00
const basenameByteLength = Buffer . byteLength ( basename , 'utf16le' )
if ( basenameByteLength + extByteLength > MAX _FILENAME _BYTES ) {
2025-03-19 17:39:23 -05:00
Logger . debug ( ` [fileUtils] Filename " ${ filename } " is too long ( ${ basenameByteLength + extByteLength } bytes), trimming basename to ${ MAX _FILENAME _BYTES - extByteLength } bytes. ` )
2022-12-13 17:46:18 -06:00
const MaxBytesForBasename = MAX _FILENAME _BYTES - extByteLength
let totalBytes = 0
let trimmedBasename = ''
// Add chars until max bytes is reached
for ( const char of basename ) {
totalBytes += Buffer . byteLength ( char , 'utf16le' )
if ( totalBytes > MaxBytesForBasename ) break
else trimmedBasename += char
}
trimmedBasename = trimmedBasename . trim ( )
sanitized = trimmedBasename + ext
2022-06-01 20:14:10 -05:00
}
2025-03-19 17:39:23 -05:00
if ( filename !== sanitized ) {
Logger . debug ( ` [fileUtils] Sanitized filename " ${ filename } " to " ${ sanitized } " ( ${ Buffer . byteLength ( sanitized , 'utf16le' ) } bytes) ` )
}
2022-03-21 19:24:38 -05:00
return sanitized
2022-06-26 15:46:16 -05:00
}
2022-07-24 13:32:05 -05:00
// Returns null if extname is not in our defined list of audio extnames
module . exports . getAudioMimeTypeFromExtname = ( extname ) => {
if ( ! extname || ! extname . length ) return null
const formatUpper = extname . slice ( 1 ) . toUpperCase ( )
if ( AudioMimeType [ formatUpper ] ) return AudioMimeType [ formatUpper ]
return null
2022-08-15 17:35:13 -05:00
}
module . exports . removeFile = ( path ) => {
if ( ! path ) return false
2024-06-03 17:21:18 -05:00
return fs
. remove ( path )
. then ( ( ) => true )
. catch ( ( error ) => {
Logger . error ( ` [fileUtils] Failed remove file " ${ path } " ` , error )
return false
} )
2023-01-05 17:45:27 -06:00
}
module . exports . encodeUriPath = ( path ) => {
2024-06-03 17:21:18 -05:00
const uri = new URL ( '/' , 'file://' )
2024-02-29 17:56:55 +01:00
// we assign the path here to assure that URL control characters like # are
// actually interpreted as part of the URL path
uri . pathname = path
2023-09-18 13:08:19 -07:00
return uri . pathname
2023-07-19 23:59:00 +02:00
}
2023-12-14 09:47:18 +02:00
/ * *
* Check if directory is writable .
* This method is necessary because fs . access ( directory , fs . constants . W _OK ) does not work on Windows
2024-06-03 17:21:18 -05:00
*
* @ param { string } directory
2024-02-17 17:40:33 -06:00
* @ returns { Promise < boolean > }
2023-12-14 09:47:18 +02:00
* /
module . exports . isWritable = async ( directory ) => {
try {
2024-01-25 17:51:06 +02:00
const accessTestFile = Path . join ( directory , 'accessTest' )
2023-12-14 09:47:18 +02:00
await fs . writeFile ( accessTestFile , '' )
await fs . remove ( accessTestFile )
return true
} catch ( err ) {
2024-01-25 17:51:06 +02:00
Logger . info ( ` [fileUtils] Directory is not writable " ${ directory } " ` , err )
2023-12-14 09:47:18 +02:00
return false
}
}
2024-01-03 16:23:17 -06:00
/ * *
* Get Windows drives as array e . g . [ "C:/" , "F:/" ]
2024-06-03 17:21:18 -05:00
*
2024-01-03 16:23:17 -06:00
* @ returns { Promise < string [ ] > }
* /
module . exports . getWindowsDrives = async ( ) => {
if ( ! global . isWin ) {
return [ ]
}
return new Promise ( ( resolve , reject ) => {
exec ( 'wmic logicaldisk get name' , async ( error , stdout , stderr ) => {
if ( error ) {
reject ( error )
return
}
2024-06-03 17:21:18 -05:00
let drives = stdout
? . split ( /\r?\n/ )
. map ( ( line ) => line . trim ( ) )
. filter ( ( line ) => line )
. slice ( 1 )
2024-01-03 16:23:17 -06:00
const validDrives = [ ]
for ( const drive of drives ) {
let drivepath = drive + '/'
if ( await fs . pathExists ( drivepath ) ) {
validDrives . push ( drivepath )
} else {
Logger . error ( ` Invalid drive ${ drivepath } ` )
}
}
resolve ( validDrives )
} )
} )
}
/ * *
* Get array of directory paths in a directory
2024-06-03 17:21:18 -05:00
*
* @ param { string } dirPath
2024-01-03 16:23:17 -06:00
* @ param { number } level
* @ returns { Promise < { path : string , dirname : string , level : number } [ ] > }
* /
module . exports . getDirectoriesInPath = async ( dirPath , level ) => {
try {
const paths = await fs . readdir ( dirPath )
2024-06-03 17:21:18 -05:00
let dirs = await Promise . all (
paths . map ( async ( dirname ) => {
const fullPath = Path . join ( dirPath , dirname )
const lstat = await fs . lstat ( fullPath ) . catch ( ( error ) => {
Logger . debug ( ` Failed to lstat " ${ fullPath } " ` , error )
return null
} )
if ( ! lstat ? . isDirectory ( ) ) return null
return {
path : this . filePathToPOSIX ( fullPath ) ,
dirname ,
level
}
2024-01-03 16:23:17 -06:00
} )
2024-06-03 17:21:18 -05:00
)
dirs = dirs . filter ( ( d ) => d )
2024-01-03 16:23:17 -06:00
return dirs
} catch ( error ) {
Logger . error ( 'Failed to readdir' , dirPath , error )
return [ ]
}
2024-06-03 17:21:18 -05:00
}
2024-07-29 20:19:58 +03:00
/ * *
* Copies a file from the source path to an existing destination path , preserving the destination ' s permissions .
*
* @ param { string } srcPath - The path of the source file .
* @ param { string } destPath - The path of the existing destination file .
* @ returns { Promise < void > } A promise that resolves when the file has been successfully copied .
* @ throws { Error } If there is an error reading the source file or writing the destination file .
* /
async function copyToExisting ( srcPath , destPath ) {
return new Promise ( ( resolve , reject ) => {
// Create a readable stream from the source file
const readStream = fs . createReadStream ( srcPath )
// Create a writable stream to the destination file
const writeStream = fs . createWriteStream ( destPath , { flags : 'w' } )
// Pipe the read stream to the write stream
readStream . pipe ( writeStream )
// Handle the end of the stream
writeStream . on ( 'finish' , ( ) => {
Logger . debug ( ` [copyToExisting] Successfully copied file from ${ srcPath } to ${ destPath } ` )
resolve ( )
} )
// Handle errors
readStream . on ( 'error' , ( error ) => {
Logger . error ( ` [copyToExisting] Error reading from source file ${ srcPath } : ${ error . message } ` )
readStream . close ( )
writeStream . close ( )
reject ( error )
} )
writeStream . on ( 'error' , ( error ) => {
Logger . error ( ` [copyToExisting] Error writing to destination file ${ destPath } : ${ error . message } ` )
readStream . close ( )
writeStream . close ( )
reject ( error )
} )
} )
}
module . exports . copyToExisting = copyToExisting