diff --git a/server/Watcher.js b/server/Watcher.js index c5f64917b..8931ae0c2 100644 --- a/server/Watcher.js +++ b/server/Watcher.js @@ -67,7 +67,8 @@ class FolderWatcher extends EventEmitter { renameTimeout: 2000, recursive: true, ignoreInitial: true, - persistent: true + persistent: true, + followSymlinks: true }) watcher .on('add', (path) => { diff --git a/server/libs/watcher/utils.js b/server/libs/watcher/utils.js index 75a2bdb22..d38e2293d 100644 --- a/server/libs/watcher/utils.js +++ b/server/libs/watcher/utils.js @@ -71,13 +71,13 @@ const Utils = { poll: (targetPath, timeout = constants_1.POLLING_TIMEOUT) => { return ripstat_1.default(targetPath, timeout).catch(Utils.lang.noop); }, - readdir: async (rootPath, ignore, depth = Infinity, signal, readdirMap) => { + readdir: async (rootPath, ignore, depth = Infinity, signal, readdirMap, followSymlinks = false) => { if (readdirMap && depth === 1 && rootPath in readdirMap) { // Reusing cached data const result = readdirMap[rootPath]; return [result.directories, result.files]; } else { // Retrieving fresh data - const result = await tiny_readdir_1.default(rootPath, { depth, ignore, signal }); + const result = await tiny_readdir_1.default(rootPath, { depth, ignore, signal, followSymlinks }); return [result.directories, result.files]; } } diff --git a/server/libs/watcher/watcher_handler.js b/server/libs/watcher/watcher_handler.js index 411e65b0d..f52b973ca 100644 --- a/server/libs/watcher/watcher_handler.js +++ b/server/libs/watcher/watcher_handler.js @@ -91,7 +91,7 @@ class WatcherHandler { var _a, _b; if (isInitial) return events; - const depth = this.options.recursive ? (_a = this.options.depth) !== null && _a !== void 0 ? _a : constants_1.DEPTH : Math.min(1, (_b = this.options.depth) !== null && _b !== void 0 ? _b : constants_1.DEPTH), [directories, files] = await utils_1.default.fs.readdir(targetPath, this.options.ignore, depth, this.watcher._closeSignal), targetSubPaths = [...directories, ...files]; + const depth = this.options.recursive ? (_a = this.options.depth) !== null && _a !== void 0 ? _a : constants_1.DEPTH : Math.min(1, (_b = this.options.depth) !== null && _b !== void 0 ? _b : constants_1.DEPTH), [directories, files] = await utils_1.default.fs.readdir(targetPath, this.options.ignore, depth, this.watcher._closeSignal, undefined, this.options.followSymlinks), targetSubPaths = [...directories, ...files]; await Promise.all(targetSubPaths.map(targetSubPath => { if (this.watcher.isIgnored(targetSubPath, this.options.ignore)) return; @@ -229,7 +229,7 @@ class WatcherHandler { await this.onWatcherEvent("change" /* CHANGE */, this.filePath, isInitial); } else { // Multiple initial paths - const depth = this.options.recursive && (constants_1.HAS_NATIVE_RECURSION && this.options.native !== false) ? (_a = this.options.depth) !== null && _a !== void 0 ? _a : constants_1.DEPTH : Math.min(1, (_b = this.options.depth) !== null && _b !== void 0 ? _b : constants_1.DEPTH), [directories, files] = await utils_1.default.fs.readdir(this.folderPath, this.options.ignore, depth, this.watcher._closeSignal, this.options.readdirMap), targetPaths = [this.folderPath, ...directories, ...files]; + const depth = this.options.recursive && (constants_1.HAS_NATIVE_RECURSION && this.options.native !== false) ? (_a = this.options.depth) !== null && _a !== void 0 ? _a : constants_1.DEPTH : Math.min(1, (_b = this.options.depth) !== null && _b !== void 0 ? _b : constants_1.DEPTH), [directories, files] = await utils_1.default.fs.readdir(this.folderPath, this.options.ignore, depth, this.watcher._closeSignal, this.options.readdirMap, this.options.followSymlinks), targetPaths = [this.folderPath, ...directories, ...files]; await Promise.all(targetPaths.map(targetPath => { if (this.watcher._poller.stats.has(targetPath)) return; // Already polled diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 9a349bd54..97d7e5d9c 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -199,7 +199,7 @@ module.exports.recurseFiles = async (path, relPathToReplace = null) => { ignoreFolders: true, extensions: true, deep: true, - realPath: true, + realPath: false, normalizePath: false } let list = await rra.list(path, options) @@ -517,7 +517,15 @@ module.exports.getDirectoriesInPath = async (dirPath, level) => { Logger.debug(`Failed to lstat "${fullPath}"`, error) return null }) - if (!lstat?.isDirectory()) return null + if (!lstat) return null + + let isDir = lstat.isDirectory() + if (!isDir && lstat.isSymbolicLink()) { + // Follow symlink to check if target is a directory + const targetStat = await fs.stat(fullPath).catch(() => null) + isDir = targetStat?.isDirectory() ?? false + } + if (!isDir) return null return { path: this.filePathToPOSIX(fullPath), diff --git a/test/server/utils/fileUtils.test.js b/test/server/utils/fileUtils.test.js index b57a6fb86..0760af652 100644 --- a/test/server/utils/fileUtils.test.js +++ b/test/server/utils/fileUtils.test.js @@ -66,7 +66,8 @@ describe('fileUtils', () => { // Stub fs.readdir readdirStub = sinon.stub(fs, 'readdir') readdirStub.callsFake((path, callback) => { - const contents = mockDirContents.get(path) + const normalizedPath = path.replace(/\/+$/, '') + const contents = mockDirContents.get(normalizedPath) if (contents) { callback(null, contents) } else {