mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-22 00:07:52 +01:00
Update:Library folder browser to also work for debian and windows
This commit is contained in:
parent
8c6a2ac5dd
commit
9f909b0d85
@ -31,7 +31,7 @@
|
||||
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
<modals-libraries-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" />
|
||||
<modals-libraries-lazy-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -4,29 +4,32 @@
|
||||
<span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span>
|
||||
<p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p>
|
||||
</div>
|
||||
<div v-if="allFolders.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
|
||||
<p class="font-mono truncate">{{ selectedPath || '\\' }}</p>
|
||||
<div v-if="rootDirs.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
|
||||
<p class="font-mono truncate">{{ selectedPath || '/' }}</p>
|
||||
</div>
|
||||
<div v-if="allFolders.length" class="flex bg-primary bg-opacity-50 p-4 folder-container">
|
||||
<div v-if="rootDirs.length" class="relative flex bg-primary bg-opacity-50 p-4 folder-container">
|
||||
<div class="w-1/2 border-r border-bg h-full overflow-y-auto">
|
||||
<div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center" @click="goBack">
|
||||
<div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center hover:bg-white/10" @click="goBack">
|
||||
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<p class="text-base font-mono px-2">..</p>
|
||||
</div>
|
||||
<div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200" :class="dir.className" @click="selectDir(dir)">
|
||||
<div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" :class="dir.className" @click="selectDir(dir)">
|
||||
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
|
||||
<span v-if="dir.dirs && dir.dirs.length && dir.path === selectedPath" class="material-icons" style="font-size: 1.1rem">arrow_right</span>
|
||||
<span v-if="dir.path === selectedPath" class="material-icons" style="font-size: 1.1rem">arrow_right</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2 h-full overflow-y-auto">
|
||||
<div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200" @click="selectSubDir(dir)">
|
||||
<div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" @click="selectSubDir(dir)">
|
||||
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loadingDirs" class="absolute inset-0 w-full h-full flex items-center justify-center bg-black/10">
|
||||
<ui-loading-indicator />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="loadingFolders" class="py-12 text-center">
|
||||
<div v-else-if="initialLoad" class="py-12 text-center">
|
||||
<p>{{ $strings.MessageLoadingFolders }}</p>
|
||||
</div>
|
||||
<div v-else class="py-12 text-center max-w-sm mx-auto">
|
||||
@ -51,11 +54,12 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loadingFolders: false,
|
||||
allFolders: [],
|
||||
initialLoad: false,
|
||||
loadingDirs: false,
|
||||
isPosix: true,
|
||||
rootDirs: [],
|
||||
directories: [],
|
||||
selectedPath: '',
|
||||
selectedFullPath: '',
|
||||
subdirs: [],
|
||||
level: 0,
|
||||
currentDir: null,
|
||||
@ -98,59 +102,88 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
var splitPaths = this.selectedPath.split('\\').slice(1)
|
||||
var prev = splitPaths.slice(0, -1).join('\\')
|
||||
async goBack() {
|
||||
let selPath = this.selectedPath.replace(/^\//, '')
|
||||
var splitPaths = selPath.split('/')
|
||||
|
||||
var currDirs = this.allFolders
|
||||
for (let i = 0; i < splitPaths.length; i++) {
|
||||
var _dir = currDirs.find((dir) => dir.dirname === splitPaths[i])
|
||||
if (_dir && _dir.path.slice(1) === prev) {
|
||||
this.directories = currDirs
|
||||
this.selectDir(_dir)
|
||||
return
|
||||
} else if (_dir) {
|
||||
currDirs = _dir.dirs
|
||||
}
|
||||
let previousPath = ''
|
||||
let lookupPath = ''
|
||||
|
||||
if (splitPaths.length > 2) {
|
||||
lookupPath = splitPaths.slice(0, -2).join('/')
|
||||
}
|
||||
previousPath = splitPaths.slice(0, -1).join('/')
|
||||
|
||||
if (!this.isPosix) {
|
||||
// For windows drives add a trailing slash. e.g. C:/
|
||||
if (!this.isPosix && lookupPath.endsWith(':')) {
|
||||
lookupPath += '/'
|
||||
}
|
||||
if (!this.isPosix && previousPath.endsWith(':')) {
|
||||
previousPath += '/'
|
||||
}
|
||||
} else {
|
||||
// Add leading slash
|
||||
if (previousPath) previousPath = '/' + previousPath
|
||||
if (lookupPath) lookupPath = '/' + lookupPath
|
||||
}
|
||||
|
||||
this.level--
|
||||
this.subdirs = this.directories
|
||||
this.selectedPath = previousPath
|
||||
this.directories = await this.fetchDirs(lookupPath, this.level)
|
||||
},
|
||||
selectDir(dir) {
|
||||
async selectDir(dir) {
|
||||
if (dir.isUsed) return
|
||||
this.selectedPath = dir.path
|
||||
this.selectedFullPath = dir.fullPath
|
||||
this.level = dir.level
|
||||
this.subdirs = dir.dirs
|
||||
this.subdirs = await this.fetchDirs(dir.path, dir.level + 1)
|
||||
},
|
||||
selectSubDir(dir) {
|
||||
async selectSubDir(dir) {
|
||||
if (dir.isUsed) return
|
||||
this.selectedPath = dir.path
|
||||
this.selectedFullPath = dir.fullPath
|
||||
this.level = dir.level
|
||||
this.directories = this.subdirs
|
||||
this.subdirs = dir.dirs
|
||||
this.subdirs = await this.fetchDirs(dir.path, dir.level + 1)
|
||||
},
|
||||
selectFolder() {
|
||||
if (!this.selectedPath) {
|
||||
console.error('No Selected path')
|
||||
return
|
||||
}
|
||||
if (this.paths.find((p) => p.startsWith(this.selectedFullPath))) {
|
||||
if (this.paths.find((p) => p.startsWith(this.selectedPath))) {
|
||||
this.$toast.error(`Oops, you cannot add a parent directory of a folder already added`)
|
||||
return
|
||||
}
|
||||
this.$emit('select', this.selectedFullPath)
|
||||
this.$emit('select', this.selectedPath)
|
||||
this.selectedPath = ''
|
||||
this.selectedFullPath = ''
|
||||
},
|
||||
fetchDirs(path, level) {
|
||||
this.loadingDirs = true
|
||||
return this.$axios
|
||||
.$get(`/api/filesystem?path=${path}&level=${level}`)
|
||||
.then((data) => {
|
||||
console.log('Fetched directories', data.directories)
|
||||
this.isPosix = !!data.posix
|
||||
return data.directories
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to get filesystem paths', error)
|
||||
this.$toast.error('Failed to get filesystem paths')
|
||||
return []
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadingDirs = false
|
||||
})
|
||||
},
|
||||
async init() {
|
||||
this.loadingFolders = true
|
||||
this.allFolders = await this.$store.dispatch('libraries/loadFolders')
|
||||
this.loadingFolders = false
|
||||
this.initialLoad = true
|
||||
this.rootDirs = await this.fetchDirs('', 0)
|
||||
this.initialLoad = false
|
||||
|
||||
this.directories = this.allFolders
|
||||
this.directories = this.rootDirs
|
||||
this.subdirs = []
|
||||
this.selectedPath = ''
|
||||
this.selectedFullPath = ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
@ -1,31 +1,69 @@
|
||||
const Path = require('path')
|
||||
const Logger = require('../Logger')
|
||||
const Database = require('../Database')
|
||||
const fs = require('../libs/fsExtra')
|
||||
const { toNumber } = require('../utils/index')
|
||||
const fileUtils = require('../utils/fileUtils')
|
||||
|
||||
class FileSystemController {
|
||||
constructor() { }
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async getPaths(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[FileSystemController] Non-admin user attempting to get filesystem paths`, req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const excludedDirs = ['node_modules', 'client', 'server', '.git', 'static', 'build', 'dist', 'metadata', 'config', 'sys', 'proc'].map(dirname => {
|
||||
return Path.sep + dirname
|
||||
})
|
||||
const relpath = req.query.path
|
||||
const level = toNumber(req.query.level, 0)
|
||||
|
||||
// Do not include existing mapped library paths in response
|
||||
const libraryFoldersPaths = await Database.libraryFolderModel.getAllLibraryFolderPaths()
|
||||
libraryFoldersPaths.forEach((path) => {
|
||||
let dir = path || ''
|
||||
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
|
||||
excludedDirs.push(dir)
|
||||
// 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({
|
||||
directories: await this.getDirectories(global.appRoot, '/', excludedDirs)
|
||||
posix: !global.isWin,
|
||||
directories
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -320,35 +320,6 @@ class ApiRouter {
|
||||
this.router.get('/stats/year/:year', MiscController.getAdminStatsForYear.bind(this))
|
||||
}
|
||||
|
||||
async getDirectories(dir, relpath, excludedDirs, level = 0) {
|
||||
try {
|
||||
const paths = await fs.readdir(dir)
|
||||
|
||||
let dirs = await Promise.all(paths.map(async dirname => {
|
||||
const fullPath = Path.join(dir, dirname)
|
||||
const path = Path.join(relpath, dirname)
|
||||
|
||||
const isDir = (await fs.lstat(fullPath)).isDirectory()
|
||||
if (isDir && !excludedDirs.includes(path) && dirname !== 'node_modules') {
|
||||
return {
|
||||
path,
|
||||
dirname,
|
||||
fullPath,
|
||||
level,
|
||||
dirs: level < 4 ? (await this.getDirectories(fullPath, path, excludedDirs, level + 1)) : []
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}))
|
||||
dirs = dirs.filter(d => d)
|
||||
return dirs
|
||||
} catch (error) {
|
||||
Logger.error('Failed to readdir', dir, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Helper Methods
|
||||
//
|
||||
|
@ -1,6 +1,7 @@
|
||||
const axios = require('axios')
|
||||
const Path = require('path')
|
||||
const ssrfFilter = require('ssrf-req-filter')
|
||||
const exec = require('child_process').exec
|
||||
const fs = require('../libs/fsExtra')
|
||||
const rra = require('../libs/recursiveReaddirAsync')
|
||||
const Logger = require('../Logger')
|
||||
@ -378,3 +379,65 @@ module.exports.isWritable = async (directory) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Windows drives as array e.g. ["C:/", "F:/"]
|
||||
*
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
module.exports.getWindowsDrives = async () => {
|
||||
if (!global.isWin) {
|
||||
return []
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
exec('wmic logicaldisk get name', async (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
let drives = stdout?.split(/\r?\n/).map(line => line.trim()).filter(line => line).slice(1)
|
||||
const validDrives = []
|
||||
for (const drive of drives) {
|
||||
let drivepath = drive + '/'
|
||||
if (await fs.pathExists(drivepath)) {
|
||||
validDrives.push(drivepath)
|
||||
} else {
|
||||
Logger.error(`Invalid drive ${drivepath}`)
|
||||
}
|
||||
}
|
||||
resolve(validDrives)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of directory paths in a directory
|
||||
*
|
||||
* @param {string} dirPath
|
||||
* @param {number} level
|
||||
* @returns {Promise<{ path:string, dirname:string, level:number }[]>}
|
||||
*/
|
||||
module.exports.getDirectoriesInPath = async (dirPath, level) => {
|
||||
try {
|
||||
const paths = await fs.readdir(dirPath)
|
||||
let dirs = await Promise.all(paths.map(async dirname => {
|
||||
const fullPath = Path.join(dirPath, dirname)
|
||||
|
||||
const lstat = await fs.lstat(fullPath).catch((error) => {
|
||||
Logger.debug(`Failed to lstat "${fullPath}"`, error)
|
||||
return null
|
||||
})
|
||||
if (!lstat?.isDirectory()) return null
|
||||
|
||||
return {
|
||||
path: this.filePathToPOSIX(fullPath),
|
||||
dirname,
|
||||
level
|
||||
}
|
||||
}))
|
||||
dirs = dirs.filter(d => d)
|
||||
return dirs
|
||||
} catch (error) {
|
||||
Logger.error('Failed to readdir', dirPath, error)
|
||||
return []
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user