From 4e67d56cd5c3bf63c28ef2240cc7e164ca0db3f8 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Thu, 12 Jun 2025 08:44:28 +0200 Subject: [PATCH] misc --- server/controllers/FileSystemController.js | 89 +++------------------- server/controllers/MiscController.js | 13 +++- server/utils/fileUtils.js | 78 +++++++++++++++++++ 3 files changed, 101 insertions(+), 79 deletions(-) diff --git a/server/controllers/FileSystemController.js b/server/controllers/FileSystemController.js index d4e6b8e0..4d87dea0 100644 --- a/server/controllers/FileSystemController.js +++ b/server/controllers/FileSystemController.js @@ -5,6 +5,7 @@ const fs = require('../libs/fsExtra') const { toNumber } = require('../utils/index') const fileUtils = require('../utils/fileUtils') const Database = require('../Database') +const { validatePathExists } = require('../utils/fileUtils') /** * @typedef RequestUserObject @@ -88,10 +89,10 @@ class FileSystemController { return res.sendStatus(403) } - // fileName - If fileName is provided, the check only returns true if the actual file exists, not just the directory + // 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 + 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({ @@ -99,10 +100,10 @@ class FileSystemController { }) } - if (fileName && typeof fileName !== 'string') { - Logger.error(`[FileSystemController] Invalid fileName in request body: ${JSON.stringify(req.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' + error: 'Invalid filename' }) } @@ -130,82 +131,14 @@ class FileSystemController { return res.sendStatus(403) } - let filepath = Path.join(libraryFolder.path, directory) - filepath = fileUtils.filePathToPOSIX(filepath) + const result = await validatePathExists(libraryFolder, directory, filename, allowBookFiles, allowAudioFiles) - // Ensure filepath is inside library folder (prevents directory traversal) (And convert libraryFolder to Path to normalize) - if (!filepath.startsWith(libraryFolder.path)) { - Logger.error(`[FileSystemController] Filepath is not inside library folder: ${filepath}`) - return res.sendStatus(400) - } + if (!result) return res.status(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) - }) + console.log(`[FileSystemController] Path exists check for "${directory}" in library "${libraryFolder.libraryId}" with filename "${filename}" returned: ${result.exists}`) - // 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 - }) + return res.json(result) } } + module.exports = new FileSystemController() diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index c779bdd6..c57e0cde 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -6,11 +6,12 @@ const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const Watcher = require('../Watcher') +const globals = require('../utils/globals') const libraryItemFilters = require('../utils/queries/libraryItemFilters') const patternValidation = require('../libs/nodeCron/pattern-validation') const { isObject, getTitleIgnorePrefix } = require('../utils/index') -const { sanitizeFilename } = require('../utils/fileUtils') +const { sanitizeFilename, validatePathExists } = require('../utils/fileUtils') const TaskManager = require('../managers/TaskManager') const adminStats = require('../utils/queries/adminStats') @@ -78,6 +79,16 @@ class MiscController { const cleanedOutputDirectoryParts = outputDirectoryParts.filter(Boolean).map((part) => sanitizeFilename(part)) const outputDirectory = Path.join(...[folder.path, ...cleanedOutputDirectoryParts]) + const containsBook = files.some(file => globals.SupportedEbookTypes.includes(Path.extname(file.name).toLowerCase())) + const containsAudio = files.some(file => globals.SupportedAudioTypes.includes(Path.extname(file.name).toLowerCase())) + + console.log(`Uploading files to ${outputDirectory} with containsBook: ${containsBook}, containsAudio: ${containsAudio}`) + + if ((await validatePathExists(folder, outputDirectory, undefined, !containsBook, !containsAudio)).exists) { + Logger.error(`Upload path already exists: ${outputDirectory}`) + return res.status(400).send('Uploaded file already exists') + } + await fs.ensureDir(outputDirectory) Logger.info(`Uploading ${files.length} files to`, outputDirectory) diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index f80c4acd..51a7c816 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -6,6 +6,7 @@ const fs = require('../libs/fsExtra') const rra = require('../libs/recursiveReaddirAsync') const Logger = require('../Logger') const { AudioMimeType } = require('./constants') +const globals = require('./globals') /** * Make sure folder separator is POSIX for Windows file paths. e.g. "C:\Users\Abs" becomes "C:/Users/Abs" @@ -577,3 +578,80 @@ async function copyToExisting(srcPath, destPath) { }) } module.exports.copyToExisting = copyToExisting + +module.exports.validatePathExists = async function validatePathExists(libraryFolder, directory, filename, allowBookFiles, allowAudioFiles) { + let filepath = Path.join(libraryFolder.path, directory) + filepath = filePathToPOSIX(filepath) + + // Ensure filepath is inside library folder (prevents directory traversal) (And convert libraryFolder to Path to normalize) + if (!filepath.startsWith(libraryFolder.path)) { + Logger.error(`[FileSystemController] Filepath is not inside library folder: ${filepath}`) + return null + } + + 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 { + exists: true, + } + } + } else if(allowBookFiles || allowAudioFiles) { + let allowedExtensions = [] + if (allowBookFiles && !allowAudioFiles) { + allowedExtensions = globals.SupportedEbookTypes + } else if (allowAudioFiles && !allowBookFiles) { + allowedExtensions = globals.SupportedAudioTypes + } 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 exists + } else { + return { + 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 { + exists: true, + libraryItemTitle: libraryItem.title + } + } + } + + return { + exists: false + } +}