mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-02-06 00:16:02 +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>
|
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -4,29 +4,32 @@
|
|||||||
<span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span>
|
<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>
|
<p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="allFolders.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
|
<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>
|
<p class="font-mono truncate">{{ selectedPath || '/' }}</p>
|
||||||
</div>
|
</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 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>
|
<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>
|
<p class="text-base font-mono px-2">..</p>
|
||||||
</div>
|
</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>
|
<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>
|
<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>
|
</div>
|
||||||
<div class="w-1/2 h-full overflow-y-auto">
|
<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>
|
<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>
|
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
<div v-else-if="loadingFolders" class="py-12 text-center">
|
<div v-else-if="initialLoad" class="py-12 text-center">
|
||||||
<p>{{ $strings.MessageLoadingFolders }}</p>
|
<p>{{ $strings.MessageLoadingFolders }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="py-12 text-center max-w-sm mx-auto">
|
<div v-else class="py-12 text-center max-w-sm mx-auto">
|
||||||
@ -51,11 +54,12 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loadingFolders: false,
|
initialLoad: false,
|
||||||
allFolders: [],
|
loadingDirs: false,
|
||||||
|
isPosix: true,
|
||||||
|
rootDirs: [],
|
||||||
directories: [],
|
directories: [],
|
||||||
selectedPath: '',
|
selectedPath: '',
|
||||||
selectedFullPath: '',
|
|
||||||
subdirs: [],
|
subdirs: [],
|
||||||
level: 0,
|
level: 0,
|
||||||
currentDir: null,
|
currentDir: null,
|
||||||
@ -98,59 +102,88 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
goBack() {
|
async goBack() {
|
||||||
var splitPaths = this.selectedPath.split('\\').slice(1)
|
let selPath = this.selectedPath.replace(/^\//, '')
|
||||||
var prev = splitPaths.slice(0, -1).join('\\')
|
var splitPaths = selPath.split('/')
|
||||||
|
|
||||||
var currDirs = this.allFolders
|
let previousPath = ''
|
||||||
for (let i = 0; i < splitPaths.length; i++) {
|
let lookupPath = ''
|
||||||
var _dir = currDirs.find((dir) => dir.dirname === splitPaths[i])
|
|
||||||
if (_dir && _dir.path.slice(1) === prev) {
|
if (splitPaths.length > 2) {
|
||||||
this.directories = currDirs
|
lookupPath = splitPaths.slice(0, -2).join('/')
|
||||||
this.selectDir(_dir)
|
|
||||||
return
|
|
||||||
} else if (_dir) {
|
|
||||||
currDirs = _dir.dirs
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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
|
if (dir.isUsed) return
|
||||||
this.selectedPath = dir.path
|
this.selectedPath = dir.path
|
||||||
this.selectedFullPath = dir.fullPath
|
|
||||||
this.level = dir.level
|
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
|
if (dir.isUsed) return
|
||||||
this.selectedPath = dir.path
|
this.selectedPath = dir.path
|
||||||
this.selectedFullPath = dir.fullPath
|
|
||||||
this.level = dir.level
|
this.level = dir.level
|
||||||
this.directories = this.subdirs
|
this.directories = this.subdirs
|
||||||
this.subdirs = dir.dirs
|
this.subdirs = await this.fetchDirs(dir.path, dir.level + 1)
|
||||||
},
|
},
|
||||||
selectFolder() {
|
selectFolder() {
|
||||||
if (!this.selectedPath) {
|
if (!this.selectedPath) {
|
||||||
console.error('No Selected path')
|
console.error('No Selected path')
|
||||||
return
|
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`)
|
this.$toast.error(`Oops, you cannot add a parent directory of a folder already added`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$emit('select', this.selectedFullPath)
|
this.$emit('select', this.selectedPath)
|
||||||
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() {
|
async init() {
|
||||||
this.loadingFolders = true
|
this.initialLoad = true
|
||||||
this.allFolders = await this.$store.dispatch('libraries/loadFolders')
|
this.rootDirs = await this.fetchDirs('', 0)
|
||||||
this.loadingFolders = false
|
this.initialLoad = false
|
||||||
|
|
||||||
this.directories = this.allFolders
|
this.directories = this.rootDirs
|
||||||
this.subdirs = []
|
this.subdirs = []
|
||||||
this.selectedPath = ''
|
this.selectedPath = ''
|
||||||
this.selectedFullPath = ''
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
@ -1,31 +1,69 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const Database = require('../Database')
|
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
|
const { toNumber } = require('../utils/index')
|
||||||
|
const fileUtils = require('../utils/fileUtils')
|
||||||
|
|
||||||
class FileSystemController {
|
class FileSystemController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
async getPaths(req, res) {
|
async getPaths(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[FileSystemController] Non-admin user attempting to get filesystem paths`, req.user)
|
Logger.error(`[FileSystemController] Non-admin user attempting to get filesystem paths`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
const excludedDirs = ['node_modules', 'client', 'server', '.git', 'static', 'build', 'dist', 'metadata', 'config', 'sys', 'proc'].map(dirname => {
|
const relpath = req.query.path
|
||||||
return Path.sep + dirname
|
const level = toNumber(req.query.level, 0)
|
||||||
})
|
|
||||||
|
|
||||||
// Do not include existing mapped library paths in response
|
// Validate path. Must be absolute
|
||||||
const libraryFoldersPaths = await Database.libraryFolderModel.getAllLibraryFolderPaths()
|
if (relpath && (!Path.isAbsolute(relpath) || !await fs.pathExists(relpath))) {
|
||||||
libraryFoldersPaths.forEach((path) => {
|
Logger.error(`[FileSystemController] Invalid path in query string "${relpath}"`)
|
||||||
let dir = path || ''
|
return res.status(400).send('Invalid "path" query string')
|
||||||
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
|
}
|
||||||
excludedDirs.push(dir)
|
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({
|
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))
|
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
|
// Helper Methods
|
||||||
//
|
//
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const ssrfFilter = require('ssrf-req-filter')
|
const ssrfFilter = require('ssrf-req-filter')
|
||||||
|
const exec = require('child_process').exec
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
const rra = require('../libs/recursiveReaddirAsync')
|
const rra = require('../libs/recursiveReaddirAsync')
|
||||||
const Logger = require('../Logger')
|
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