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-05-26 16:56:50 -05:00
const { directory , folderPath } = req . body
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-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-05-26 16:56:50 -05:00
// Ensure filepath is inside library folder (prevents directory traversal)
if ( ! filepath . startsWith ( libraryFolder . path ) ) {
Logger . error ( ` [FileSystemController] Filepath is not inside library folder: ${ filepath } ` )
return res . sendStatus ( 400 )
}
if ( await fs . pathExists ( filepath ) ) {
2025-03-24 18:01:38 -05:00
return res . json ( {
exists : true
} )
}
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-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 ( )