mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-06-23 01:18:59 +02:00
206 lines
7.2 KiB
JavaScript
206 lines
7.2 KiB
JavaScript
const { Request, Response } = require('express')
|
|
const Path = require('path')
|
|
const Logger = require('../Logger')
|
|
const fs = require('../libs/fsExtra')
|
|
const { toNumber } = require('../utils/index')
|
|
const fileUtils = require('../utils/fileUtils')
|
|
const Database = require('../Database')
|
|
|
|
/**
|
|
* @typedef RequestUserObject
|
|
* @property {import('../models/User')} user
|
|
*
|
|
* @typedef {Request & RequestUserObject} RequestWithUser
|
|
*/
|
|
|
|
class FileSystemController {
|
|
constructor() {}
|
|
|
|
/**
|
|
*
|
|
* @param {RequestWithUser} req
|
|
* @param {Response} res
|
|
*/
|
|
async getPaths(req, res) {
|
|
if (!req.user.isAdminOrUp) {
|
|
Logger.error(`[FileSystemController] Non-admin user "${req.user.username}" attempting to get filesystem paths`)
|
|
return res.sendStatus(403)
|
|
}
|
|
|
|
const relpath = req.query.path
|
|
const level = toNumber(req.query.level, 0)
|
|
|
|
// Validate path. Must be absolute
|
|
if (relpath && (!Path.isAbsolute(relpath) || !(await fs.pathExists(relpath)))) {
|
|
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 = []
|
|
|
|
// 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) {
|
|
directories = drives.map((d) => {
|
|
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
|
|
const excludedDirs = ['node_modules', 'client', 'server', '.git', 'static', 'build', 'dist', 'metadata', 'config', 'sys', 'proc', '.devcontainer', '.nyc_output', '.github', '.vscode'].map((dirname) => {
|
|
return fileUtils.filePathToPOSIX(Path.join(global.appRoot, dirname))
|
|
})
|
|
directories = directories.filter((dir) => {
|
|
return !excludedDirs.includes(dir.path)
|
|
})
|
|
|
|
res.json({
|
|
posix: !global.isWin,
|
|
directories
|
|
})
|
|
}
|
|
|
|
/**
|
|
* POST: /api/filesystem/pathexists
|
|
*
|
|
* @param {RequestWithUser} req
|
|
* @param {Response} res
|
|
*/
|
|
async checkPathExists(req, res) {
|
|
if (!req.user.canUpload) {
|
|
Logger.error(`[FileSystemController] User "${req.user.username}" without upload permissions attempting to check path exists`)
|
|
return res.sendStatus(403)
|
|
}
|
|
|
|
// 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
|
|
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'
|
|
})
|
|
}
|
|
|
|
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'
|
|
})
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
const filepath = Path.join(libraryFolder.path, directory)
|
|
|
|
// Ensure filepath is inside library folder (prevents directory traversal) (And convert libraryFolder to Path to normalize)
|
|
if (!filepath.startsWith(Path.join(libraryFolder.path))) {
|
|
Logger.error(`[FileSystemController] Filepath is not inside library folder: ${filepath}`)
|
|
return res.sendStatus(400)
|
|
}
|
|
|
|
if (await fs.pathExists(filepath)) {
|
|
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
|
|
})
|
|
}
|
|
}
|
|
|
|
// Check if a library item exists in a subdirectory
|
|
// See: https://github.com/advplyr/audiobookshelf/issues/4146
|
|
// For filenames it does not matter if the file is in a subdirectory or not because the file is not allowed to be created
|
|
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
|
|
}
|
|
})
|
|
|
|
if (libraryItem) {
|
|
return res.json({
|
|
exists: true,
|
|
libraryItemTitle: libraryItem.title
|
|
})
|
|
}
|
|
}
|
|
|
|
return res.json({
|
|
exists: false
|
|
})
|
|
}
|
|
}
|
|
module.exports = new FileSystemController()
|