mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-07-26 13:51:16 +02:00
Merge 4b91a7e3f9
into 28404f37b8
This commit is contained in:
commit
0968511255
@ -91,6 +91,7 @@
|
||||
<script>
|
||||
import Path from 'path'
|
||||
import uploadHelpers from '@/mixins/uploadHelpers'
|
||||
import globals from '../../../server/utils/globals'
|
||||
|
||||
export default {
|
||||
mixins: [uploadHelpers],
|
||||
@ -359,8 +360,15 @@ export default {
|
||||
// Check if path already exists before starting upload
|
||||
// uploading fails if path already exists
|
||||
for (const item of items) {
|
||||
const containsBook = item.files.some(file => globals.SupportedEbookTypes.includes(Path.extname(file.name).toLowerCase().slice(1)))
|
||||
const containsAudio = item.files.some(file => globals.SupportedAudioTypes.includes(Path.extname(file.name).toLowerCase().slice(1)))
|
||||
|
||||
const exists = await this.$axios
|
||||
.$post(`/api/filesystem/pathexists`, { directory: item.directory, folderPath: this.selectedFolder.fullPath })
|
||||
.$post(`/api/filesystem/pathexists`, { directory: item.directory, folderPath: this.selectedFolder.fullPath, filenames: item.files.map((f) => f.name),
|
||||
...(this.selectedLibrary.mediaType === 'podcast'
|
||||
? { allowBookFiles: !containsBook, allowAudioFiles: true }
|
||||
: { allowBookFiles: !containsBook, allowAudioFiles: !containsAudio })
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.exists) {
|
||||
if (data.libraryItemTitle) {
|
||||
|
@ -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,7 +89,11 @@ class FileSystemController {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const { directory, folderPath } = req.body
|
||||
// filenames - If filenames is provided, the check only returns true if the actual files exist, 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, filenames, 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({
|
||||
@ -96,6 +101,21 @@ class FileSystemController {
|
||||
})
|
||||
}
|
||||
|
||||
// Validate filenames: must be undefined or an array of non-empty strings
|
||||
if (filenames !== undefined && (!Array.isArray(filenames) || filenames.some((f) => typeof f !== 'string' || f.trim().length === 0))) {
|
||||
Logger.error(`[FileSystemController] Invalid filenames in request body: ${JSON.stringify(req.body)}`)
|
||||
return res.status(400).json({
|
||||
error: 'Invalid filenames'
|
||||
})
|
||||
}
|
||||
|
||||
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: {
|
||||
@ -113,50 +133,14 @@ class FileSystemController {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
let filepath = Path.join(libraryFolder.path, directory)
|
||||
filepath = fileUtils.filePathToPOSIX(filepath)
|
||||
const result = await validatePathExists(libraryFolder, directory, filenames, allowBookFiles, allowAudioFiles)
|
||||
|
||||
// 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 (!result) return res.status(400).end()
|
||||
|
||||
if (await fs.pathExists(filepath)) {
|
||||
return res.json({
|
||||
exists: true
|
||||
})
|
||||
}
|
||||
console.log(`[FileSystemController] Path exists check for "${directory}" in library "${libraryFolder.libraryId}" with filenames "${Array.isArray(filenames) ? filenames.join(', ') : 'N/A'}", allowBookFiles: ${allowBookFiles}, allowAudioFiles: ${allowAudioFiles} - Result: ${result.exists}`)
|
||||
|
||||
// Check if a library item exists in a subdirectory
|
||||
// See: https://github.com/advplyr/audiobookshelf/issues/4146
|
||||
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()
|
||||
|
@ -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')
|
||||
@ -43,7 +44,8 @@ class MiscController {
|
||||
}
|
||||
|
||||
const files = Object.values(req.files)
|
||||
let { title, author, series, folder: folderId, library: libraryId } = req.body
|
||||
// If allowOverwrite is true, it will allow overwriting existing files.
|
||||
let { title, author, series, folder: folderId, library: libraryId, allowOverwrite } = req.body
|
||||
// Validate request body
|
||||
if (!libraryId || !folderId || typeof libraryId !== 'string' || typeof folderId !== 'string' || !title || typeof title !== 'string') {
|
||||
return res.status(400).send('Invalid request body')
|
||||
@ -78,6 +80,16 @@ class MiscController {
|
||||
const cleanedOutputDirectoryParts = outputDirectoryParts.filter(Boolean).map((part) => sanitizeFilename(part))
|
||||
const outputDirectory = Path.join(...[folder.path, ...cleanedOutputDirectoryParts])
|
||||
|
||||
if (allowOverwrite === undefined || allowOverwrite === null || !allowOverwrite) {
|
||||
const containsBook = files.some(file => globals.SupportedEbookTypes.includes(Path.extname(file.name).toLowerCase().slice(1)))
|
||||
const containsAudio = files.some(file => globals.SupportedAudioTypes.includes(Path.extname(file.name).toLowerCase().slice(1)))
|
||||
|
||||
if ((await validatePathExists(folder, outputDirectory, files.map((f) => f.name), !containsBook, library.mediaType === 'podcast' || !containsAudio, true)).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)
|
||||
|
@ -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,90 @@ async function copyToExisting(srcPath, destPath) {
|
||||
})
|
||||
}
|
||||
module.exports.copyToExisting = copyToExisting
|
||||
|
||||
module.exports.validatePathExists = async function validatePathExists(
|
||||
libraryFolder,
|
||||
directory,
|
||||
filenames,
|
||||
allowBookFiles,
|
||||
allowAudioFiles,
|
||||
skipLibraryFolder = false
|
||||
) {
|
||||
let filepath = Path.join(skipLibraryFolder ? '' : libraryFolder.path, directory);
|
||||
filepath = filePathToPOSIX(filepath);
|
||||
|
||||
// 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 null;
|
||||
}
|
||||
|
||||
if (await fs.pathExists(filepath)) {
|
||||
if (filenames && filenames.length > 0) {
|
||||
// If any filename exists, not allowed to upload (exists: true)
|
||||
for (const filename of filenames) {
|
||||
const filePath = Path.join(filepath, filename);
|
||||
if (await fs.pathExists(filePath)) {
|
||||
return {
|
||||
exists: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (allowBookFiles || allowAudioFiles) {
|
||||
let restrictedExtensions = [];
|
||||
if (allowBookFiles && !allowAudioFiles) {
|
||||
restrictedExtensions = globals.SupportedAudioTypes;
|
||||
} else if (allowAudioFiles && !allowBookFiles) {
|
||||
restrictedExtensions = globals.SupportedEbookTypes;
|
||||
} else {
|
||||
restrictedExtensions = []
|
||||
}
|
||||
|
||||
if (restrictedExtensions.length > 0) {
|
||||
const files = await fs.readdir(filepath);
|
||||
const hasRestrictedFiles = files.some((file) => {
|
||||
const ext = Path.extname(file).toLowerCase().replace(/^\./, "");
|
||||
return restrictedExtensions.includes(ext);
|
||||
});
|
||||
|
||||
if (hasRestrictedFiles) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user