diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index b35a3a99..4b6915b7 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -138,14 +138,6 @@ module.exports.readTextFile = readTextFile * @returns {string} */ module.exports.shouldIgnoreFile = (path) => { - var extensionIgnores = ['.part', '.tmp', '.crdownload', '.download', '.bak', '.old', '.temp', '.tempfile', '.tempfile~'] - - // Check extension - if (extensionIgnores.includes(Path.extname(path).toLowerCase())) { - // Return the extension that is ignored - return `${Path.extname(path)} file` - } - // Check if directory or file name starts with "." if (Path.basename(path).startsWith('.')) { return 'dotfile' @@ -155,16 +147,23 @@ module.exports.shouldIgnoreFile = (path) => { } // If these strings exist anywhere in the filename or directory name, ignore. Vendor specific hidden directories - var includeAnywhereIgnore = ['@eaDir'] - var filteredInclude = includeAnywhereIgnore.filter((str) => path.includes(str)) + const includeAnywhereIgnore = ['@eaDir'] + const filteredInclude = includeAnywhereIgnore.filter((str) => path.includes(str)) if (filteredInclude.length) { return `${filteredInclude[0]} directory` } + const extensionIgnores = ['.part', '.tmp', '.crdownload', '.download', '.bak', '.old', '.temp', '.tempfile', '.tempfile~'] + + // Check extension + if (extensionIgnores.includes(Path.extname(path).toLowerCase())) { + // Return the extension that is ignored + return `${Path.extname(path)} file` + } + // Should not ignore this file or directory return null } -module.exports.shouldIgnoreFile = this.shouldIgnoreFile /** * @typedef FilePathItem @@ -182,7 +181,7 @@ module.exports.shouldIgnoreFile = this.shouldIgnoreFile * @param {string} [relPathToReplace] * @returns {FilePathItem[]} */ -async function recurseFiles(path, relPathToReplace = null) { +module.exports.recurseFiles = async (path, relPathToReplace = null) => { path = filePathToPOSIX(path) if (!path.endsWith('/')) path = path + '/' @@ -266,7 +265,6 @@ async function recurseFiles(path, relPathToReplace = null) { return list } -module.exports.recurseFiles = recurseFiles /** * diff --git a/test/server/utils/fileUtils.test.js b/test/server/utils/fileUtils.test.js new file mode 100644 index 00000000..59907be9 --- /dev/null +++ b/test/server/utils/fileUtils.test.js @@ -0,0 +1,127 @@ +const chai = require('chai') +const expect = chai.expect +const sinon = require('sinon') +const fileUtils = require('../../../server/utils/fileUtils') +const fs = require('fs') +const Logger = require('../../../server/Logger') + +describe('fileUtils', () => { + it('shouldIgnoreFile', () => { + global.isWin = process.platform === 'win32' + + const testCases = [ + { path: 'test.txt', expected: null }, + { path: 'folder/test.mp3', expected: null }, + { path: 'normal/path/file.m4b', expected: null }, + { path: 'test.txt.part', expected: '.part file' }, + { path: 'test.txt.tmp', expected: '.tmp file' }, + { path: 'test.txt.crdownload', expected: '.crdownload file' }, + { path: 'test.txt.download', expected: '.download file' }, + { path: 'test.txt.bak', expected: '.bak file' }, + { path: 'test.txt.old', expected: '.old file' }, + { path: 'test.txt.temp', expected: '.temp file' }, + { path: 'test.txt.tempfile', expected: '.tempfile file' }, + { path: 'test.txt.tempfile~', expected: '.tempfile~ file' }, + { path: '.gitignore', expected: 'dotfile' }, + { path: 'folder/.hidden', expected: 'dotfile' }, + { path: '.git/config', expected: 'dotpath' }, + { path: 'path/.hidden/file.txt', expected: 'dotpath' }, + { path: '@eaDir', expected: '@eaDir directory' }, + { path: 'folder/@eaDir', expected: '@eaDir directory' }, + { path: 'path/@eaDir/file.txt', expected: '@eaDir directory' }, + { path: '.hidden/test.tmp', expected: 'dotpath' }, + { path: '@eaDir/test.part', expected: '@eaDir directory' } + ] + + testCases.forEach(({ path, expected }) => { + const result = fileUtils.shouldIgnoreFile(path) + expect(result).to.equal(expected) + }) + }) + + describe('recurseFiles', () => { + let readdirStub, realpathStub, statStub + + beforeEach(() => { + global.isWin = process.platform === 'win32' + + // Mock file structure with normalized paths + const mockDirContents = new Map([ + ['/test', ['file1.mp3', 'subfolder', 'ignoreme', 'temp.mp3.tmp']], + ['/test/subfolder', ['file2.m4b']], + ['/test/ignoreme', ['.ignore', 'ignored.mp3']] + ]) + + const mockStats = new Map([ + ['/test/file1.mp3', { isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '1' }], + ['/test/subfolder', { isDirectory: () => true, size: 0, mtimeMs: Date.now(), ino: '2' }], + ['/test/subfolder/file2.m4b', { isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '3' }], + ['/test/ignoreme', { isDirectory: () => true, size: 0, mtimeMs: Date.now(), ino: '4' }], + ['/test/ignoreme/.ignore', { isDirectory: () => false, size: 0, mtimeMs: Date.now(), ino: '5' }], + ['/test/ignoreme/ignored.mp3', { isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '6' }], + ['/test/temp.mp3.tmp', { isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '7' }] + ]) + + // Stub fs.readdir + readdirStub = sinon.stub(fs, 'readdir') + readdirStub.callsFake((path, callback) => { + const contents = mockDirContents.get(path) + if (contents) { + callback(null, contents) + } else { + callback(new Error(`ENOENT: no such file or directory, scandir '${path}'`)) + } + }) + + // Stub fs.realpath + realpathStub = sinon.stub(fs, 'realpath') + realpathStub.callsFake((path, callback) => { + // Return normalized path + callback(null, fileUtils.filePathToPOSIX(path).replace(/\/$/, '')) + }) + + // Stub fs.stat + statStub = sinon.stub(fs, 'stat') + statStub.callsFake((path, callback) => { + const normalizedPath = fileUtils.filePathToPOSIX(path).replace(/\/$/, '') + const stats = mockStats.get(normalizedPath) + if (stats) { + callback(null, stats) + } else { + callback(new Error(`ENOENT: no such file or directory, stat '${normalizedPath}'`)) + } + }) + + // Stub Logger + sinon.stub(Logger, 'debug') + }) + + afterEach(() => { + sinon.restore() + }) + + it('should return filtered file list', async () => { + const files = await fileUtils.recurseFiles('/test') + expect(files).to.be.an('array') + expect(files).to.have.lengthOf(2) + + expect(files[0]).to.deep.equal({ + name: 'file1.mp3', + path: 'file1.mp3', + reldirpath: '', + fullpath: '/test/file1.mp3', + extension: '.mp3', + deep: 0 + }) + + expect(files[1]).to.deep.equal({ + name: 'file2.m4b', + path: 'subfolder/file2.m4b', + reldirpath: 'subfolder', + fullpath: '/test/subfolder/file2.m4b', + extension: '.m4b', + deep: 1 + }) + }) + }) +})