2024-08-11 17:01:25 -05:00
const { Request , Response } = require ( 'express' )
2021-12-27 17:51:19 +01:00
const Path = require ( 'path' )
2023-05-27 16:00:34 -05:00
const Logger = require ( '../Logger' )
const fs = require ( '../libs/fsExtra' )
2024-01-03 16:23:17 -06:00
const { toNumber } = require ( '../utils/index' )
const fileUtils = require ( '../utils/fileUtils' )
2025-03-24 18:01:38 -05:00
const Database = require ( '../Database' )
2021-12-26 11:25:07 -06:00
2024-08-11 17:01:25 -05:00
/ * *
* @ typedef RequestUserObject
* @ property { import ( '../models/User' ) } user
*
* @ typedef { Request & RequestUserObject } RequestWithUser
* /
2021-12-26 11:25:07 -06:00
class FileSystemController {
2024-08-10 17:15:21 -05:00
constructor ( ) { }
2021-12-26 11:25:07 -06:00
2024-01-03 16:23:17 -06:00
/ * *
2024-08-10 17:15:21 -05:00
*
2024-08-11 17:01:25 -05:00
* @ param { RequestWithUser } req
* @ param { Response } res
2024-01-03 16:23:17 -06:00
* /
2021-12-26 11:25:07 -06:00
async getPaths ( req , res ) {
2024-08-11 16:07:29 -05:00
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [FileSystemController] Non-admin user " ${ req . user . username } " attempting to get filesystem paths ` )
2023-05-27 16:00:34 -05:00
return res . sendStatus ( 403 )
}
2024-01-03 16:23:17 -06:00
const relpath = req . query . path
const level = toNumber ( req . query . level , 0 )
// Validate path. Must be absolute
2024-08-10 17:15:21 -05:00
if ( relpath && ( ! Path . isAbsolute ( relpath ) || ! ( await fs . pathExists ( relpath ) ) ) ) {
2024-01-03 16:23:17 -06:00
Logger . error ( ` [FileSystemController] Invalid path in query string " ${ relpath } " ` )
return res . status ( 400 ) . send ( 'Invalid "path" query string' )
}
Logger . debug ( ` [FileSystemController] Getting file paths at ${ relpath || 'root' } ( ${ level } ) ` )
let directories = [ ]
2021-12-26 11:25:07 -06:00
2024-01-03 16:23:17 -06:00
// Windows returns drives first
if ( global . isWin ) {
if ( relpath ) {
directories = await fileUtils . getDirectoriesInPath ( relpath , level )
} else {
const drives = await fileUtils . getWindowsDrives ( ) . catch ( ( error ) => {
Logger . error ( ` [FileSystemController] Failed to get windows drives ` , error )
return [ ]
} )
if ( drives . length ) {
2024-08-10 17:15:21 -05:00
directories = drives . map ( ( d ) => {
2024-01-03 16:23:17 -06:00
return {
path : d ,
dirname : d ,
level : 0
}
} )
}
}
} else {
directories = await fileUtils . getDirectoriesInPath ( relpath || '/' , level )
}
// Exclude some dirs from this project to be cleaner in Docker
2024-08-10 17:15:21 -05:00
const excludedDirs = [ 'node_modules' , 'client' , 'server' , '.git' , 'static' , 'build' , 'dist' , 'metadata' , 'config' , 'sys' , 'proc' , '.devcontainer' , '.nyc_output' , '.github' , '.vscode' ] . map ( ( dirname ) => {
2024-01-03 16:23:17 -06:00
return fileUtils . filePathToPOSIX ( Path . join ( global . appRoot , dirname ) )
} )
2024-08-10 17:15:21 -05:00
directories = directories . filter ( ( dir ) => {
2024-01-03 16:23:17 -06:00
return ! excludedDirs . includes ( dir . path )
2021-12-26 11:25:07 -06:00
} )
2022-11-29 11:55:22 -06:00
res . json ( {
2024-01-03 16:23:17 -06:00
posix : ! global . isWin ,
directories
2022-11-29 11:55:22 -06:00
} )
2021-12-26 11:25:07 -06:00
}
2023-05-27 16:00:34 -05:00
2024-08-11 17:01:25 -05:00
/ * *
* POST : / a p i / f i l e s y s t e m / p a t h e x i s t s
*
* @ param { RequestWithUser } req
* @ param { Response } res
* /
2023-05-27 16:00:34 -05:00
async checkPathExists ( req , res ) {
2024-08-11 16:07:29 -05:00
if ( ! req . user . canUpload ) {
2025-05-26 16:56:50 -05:00
Logger . error ( ` [FileSystemController] User " ${ req . user . username } " without upload permissions attempting to check path exists ` )
2023-05-27 16:00:34 -05:00
return res . sendStatus ( 403 )
}
2025-06-11 23:08:41 +02:00
// fileName - If fileName is provided, the check only returns true if the actual file exists, not just the directory
// allowBookFiles - If true, allows containing other book related files (e.g. .pdf, .epub, etc.)
// allowAudioFiles - If true, allows containing other audio related files (e.g. .mp3, .m4b, etc.)
const { directory , folderPath , fileName , allowBookFiles , allowAudioFiles } = req . body
2025-05-26 16:56:50 -05:00
if ( ! directory ? . length || typeof directory !== 'string' || ! folderPath ? . length || typeof folderPath !== 'string' ) {
Logger . error ( ` [FileSystemController] Invalid request body: ${ JSON . stringify ( req . body ) } ` )
return res . status ( 400 ) . json ( {
error : 'Invalid request body'
} )
2023-05-27 16:00:34 -05:00
}
2025-06-11 23:08:41 +02:00
if ( fileName && typeof fileName !== 'string' ) {
Logger . error ( ` [FileSystemController] Invalid fileName in request body: ${ JSON . stringify ( req . body ) } ` )
return res . status ( 400 ) . json ( {
error : 'Invalid fileName'
} )
}
if ( allowBookFiles && typeof allowBookFiles !== 'boolean' || allowAudioFiles && typeof allowAudioFiles !== 'boolean' || ( allowBookFiles && allowAudioFiles ) ) {
Logger . error ( ` [FileSystemController] Invalid allowBookFiles or allowAudioFiles in request body: ${ JSON . stringify ( req . body ) } ` )
return res . status ( 400 ) . json ( {
error : 'Invalid allowBookFiles or allowAudioFiles'
} )
}
2025-05-26 16:56:50 -05:00
// Check that library folder exists
const libraryFolder = await Database . libraryFolderModel . findOne ( {
where : {
path : folderPath
}
} )
if ( ! libraryFolder ) {
Logger . error ( ` [FileSystemController] Library folder not found: ${ folderPath } ` )
return res . sendStatus ( 404 )
}
2025-03-24 18:01:38 -05:00
2025-06-11 16:04:18 -05:00
if ( ! req . user . checkCanAccessLibrary ( libraryFolder . libraryId ) ) {
Logger . error ( ` [FileSystemController] User " ${ req . user . username } " attempting to check path exists for library " ${ libraryFolder . libraryId } " without access ` )
return res . sendStatus ( 403 )
}
2025-06-11 16:37:07 -05:00
let filepath = Path . join ( libraryFolder . path , directory )
filepath = fileUtils . filePathToPOSIX ( filepath )
2025-06-10 17:02:42 -05:00
2025-06-11 23:08:41 +02:00
// Ensure filepath is inside library folder (prevents directory traversal) (And convert libraryFolder to Path to normalize)
if ( ! filepath . startsWith ( Path . join ( libraryFolder . path ) ) ) {
2025-05-26 16:56:50 -05:00
Logger . error ( ` [FileSystemController] Filepath is not inside library folder: ${ filepath } ` )
return res . sendStatus ( 400 )
}
if ( await fs . pathExists ( filepath ) ) {
2025-06-11 23:08:41 +02:00
if ( fileName ) {
// Check if a specific file exists
const filePath = Path . join ( filepath , fileName )
if ( await fs . pathExists ( filePath ) ) {
return res . json ( {
exists : true ,
} )
}
} else if ( allowBookFiles || allowAudioFiles ) {
let allowedExtensions = [ ]
if ( allowBookFiles && ! allowAudioFiles ) {
allowedExtensions = [ 'epub' , 'pdf' , 'mobi' , 'azw3' , 'cbr' , 'cbz' ]
} else if ( allowAudioFiles && ! allowBookFiles ) {
allowedExtensions = [ 'm4b' , 'mp3' , 'm4a' , 'flac' , 'opus' , 'ogg' , 'oga' , 'mp4' , 'aac' , 'wma' , 'aiff' , 'aif' , 'wav' , 'webm' , 'webma' , 'mka' , 'awb' , 'caf' , 'mpeg' , 'mpg' ]
} else {
allowedExtensions = [ ]
}
const files = await fs . readdir ( filepath )
const exists = allowedExtensions . length === 0
? files . length > 0
: files . some ( ( file ) => {
const ext = Path . extname ( file ) . toLowerCase ( ) . replace ( /^\./ , '' )
return allowedExtensions . includes ( ext )
} )
// To let the sub dir check run
if ( exists ) return res . json ( {
exists : exists
} )
} else {
return res . json ( {
exists : true
} )
}
2025-03-24 18:01:38 -05:00
}
2025-05-26 16:56:50 -05:00
// Check if a library item exists in a subdirectory
2025-03-24 18:01:38 -05:00
// See: https://github.com/advplyr/audiobookshelf/issues/4146
2025-06-11 23:08:41 +02:00
// For filenames it does not matter if the file is in a subdirectory or not because the file is not allowed to be created
2025-05-26 16:56:50 -05:00
const cleanedDirectory = directory . split ( '/' ) . filter ( Boolean ) . join ( '/' )
if ( cleanedDirectory . includes ( '/' ) ) {
// Can only be 2 levels deep
const possiblePaths = [ ]
const subdir = Path . dirname ( directory )
possiblePaths . push ( fileUtils . filePathToPOSIX ( Path . join ( folderPath , subdir ) ) )
if ( subdir . includes ( '/' ) ) {
possiblePaths . push ( fileUtils . filePathToPOSIX ( Path . join ( folderPath , Path . dirname ( subdir ) ) ) )
}
const libraryItem = await Database . libraryItemModel . findOne ( {
where : {
path : possiblePaths
2025-03-24 18:01:38 -05:00
}
2025-05-26 16:56:50 -05:00
} )
2025-03-24 18:01:38 -05:00
2025-05-26 16:56:50 -05:00
if ( libraryItem ) {
return res . json ( {
exists : true ,
libraryItemTitle : libraryItem . title
2025-03-24 18:01:38 -05:00
} )
}
}
return res . json ( {
exists : false
2023-05-27 16:00:34 -05:00
} )
}
2021-12-26 11:25:07 -06:00
}
2024-08-10 17:15:21 -05:00
module . exports = new FileSystemController ( )