+
{{ $strings.MessageLoadingFolders }}
@@ -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() {
diff --git a/server/controllers/FileSystemController.js b/server/controllers/FileSystemController.js
index cee52cb2..88459e51 100644
--- a/server/controllers/FileSystemController.js
+++ b/server/controllers/FileSystemController.js
@@ -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
})
}
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index 3edce256..2956cd52 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -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
//
diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js
index 89ad9e60..14e4d743 100644
--- a/server/utils/fileUtils.js
+++ b/server/utils/fileUtils.js
@@ -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}
+ */
+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 []
+ }
+}
\ No newline at end of file