From 34b957fb7eab53ff4afe50f13b6eb6f1cb8da7d6 Mon Sep 17 00:00:00 2001 From: Romain Date: Sun, 2 Apr 2023 22:11:28 +0000 Subject: [PATCH] fix: supports nested folders for --include (fixes #332) --- src/components/index/glob.js | 2 +- src/components/index/pattern.js | 19 ++++- test/cli/options.spec.js | 7 ++ test/components/index/glob.spec.js | 45 +++++++++++ test/components/index/pattern.spec.js | 103 +++++++++++++++++++++++++- 5 files changed, 172 insertions(+), 4 deletions(-) diff --git a/src/components/index/glob.js b/src/components/index/glob.js index c549d01..f712f86 100644 --- a/src/components/index/glob.js +++ b/src/components/index/glob.js @@ -16,7 +16,7 @@ const RAW_PHOTO_EXT = [ exports.find = function (rootFolder, options, callback) { const entries = {} const pattern = new GlobPattern({ - include: (options.include && options.include.length > 0) ? options.include : '**/**', + include: (options.include && options.include.length > 0) ? options.include : ['**/**'], exclude: options.exclude || [], extensions: exports.supportedExtensions(options) }) diff --git a/src/components/index/pattern.js b/src/components/index/pattern.js index aaa5456..f59f33d 100644 --- a/src/components/index/pattern.js +++ b/src/components/index/pattern.js @@ -1,9 +1,12 @@ +const _ = require('lodash') +const path = require('path') const micromatch = require('micromatch') class GlobPattern { constructor ({ include, exclude, extensions }) { this.includeList = include this.excludeList = exclude + this.includeFolders = _.uniq(_.flatMap(this.includeList, this.subFolders)) this.directoryExcludeList = exclude.concat(['**/@eaDir/**', '#recycle/**']) this.extensions = extPattern(extensions) } @@ -20,9 +23,23 @@ class GlobPattern { canTraverse (folderPath) { const opts = { dot: false, nocase: true } const withSlash = `${folderPath}/` - return micromatch.any(withSlash, this.includeList, opts) && + return micromatch.any(withSlash, this.includeFolders, opts) && micromatch.any(withSlash, this.directoryExcludeList, opts) === false } + + // returns the list of all folder names in a path + // so they can be included in traversal + subFolders (filepath) { + // keep the required path if it allows traversal (thing/ or thing/**) + const list = filepath.match(/(\/$)|(\*\*$)/) ? [filepath] : [] + // then find all parent folders + let dir = path.dirname(filepath) + while (dir !== '.' && dir !== '/') { + list.push(dir + '/') + dir = path.dirname(dir) + } + return list + } } function extPattern (extensions) { diff --git a/test/cli/options.spec.js b/test/cli/options.spec.js index df11c3b..911773c 100644 --- a/test/cli/options.spec.js +++ b/test/cli/options.spec.js @@ -141,6 +141,13 @@ describe('options', function () { should(opts.logFile).eql(path.join(process.cwd(), 'custom.log')) }) }) + describe('includes', () => { + it('always creates an array', () => { + const args = BASE_ARGS.concat(['--include', 'holidays/**']) + const opts = options.get(args) + should(opts.include).eql(['holidays/**']) + }) + }) }) describe('deprecated', () => { it('--original-photos false', () => { diff --git a/test/components/index/glob.spec.js b/test/components/index/glob.spec.js index 98313c6..f9b8040 100644 --- a/test/components/index/glob.spec.js +++ b/test/components/index/glob.spec.js @@ -176,6 +176,51 @@ describe('Index: glob', function () { ], done) }) + it('can include deep subfolders', (done) => { + mock({ + 'media/work/IMG_0001.jpg': '...', + 'media/holidays/venice/IMG_0002.jpg': '...' + }) + const options = { + include: [ + 'holidays/**' + ] + } + assertGlobReturns('media', options, [ + 'holidays/venice/IMG_0002.jpg' + ], done) + }) + + it('can include nested subfolders', (done) => { + mock({ + 'media/work/IMG_0001.jpg': '...', + 'media/holidays/venice/IMG_0002.jpg': '...' + }) + const options = { + include: [ + 'holidays/venice/**' + ] + } + assertGlobReturns('media', options, [ + 'holidays/venice/IMG_0002.jpg' + ], done) + }) + + it('can include a specific file by path', (done) => { + mock({ + 'media/work/IMG_0001.jpg': '...', + 'media/holidays/venice/IMG_0002.jpg': '...' + }) + const options = { + include: [ + 'holidays/venice/IMG_0002.jpg' + ] + } + assertGlobReturns('media', options, [ + 'holidays/venice/IMG_0002.jpg' + ], done) + }) + it('can specify an exclude pattern', (done) => { mock({ 'media/work/IMG_0001.jpg': '...', diff --git a/test/components/index/pattern.spec.js b/test/components/index/pattern.spec.js index 631be97..5418949 100644 --- a/test/components/index/pattern.spec.js +++ b/test/components/index/pattern.spec.js @@ -37,6 +37,7 @@ describe('Index: pattern', function () { extensions: ['jpg'] }) should(pattern.match('holidays/IMG_0001.jpg')).eql(true) + should(pattern.match('holidays/venice/IMG_0001.jpg')).eql(true) }) it('matches files that meet one of the include patterns', () => { @@ -64,6 +65,7 @@ describe('Index: pattern', function () { extensions: ['jpg'] }) should(pattern.match('holidays/IMG_0001.jpg')).eql(true) + should(pattern.match('holidays/venice/IMG_0001.jpg')).eql(true) }) it('rejects files that dont meet any of the include patterns', () => { @@ -103,6 +105,67 @@ describe('Index: pattern', function () { }) }) + describe('calculating sub-folders for traversal', () => { + it('includes all sub-folders', () => { + const pattern = new GlobPattern({ + include: ['holidays/venice/IMG001.jpg'], + exclude: [], + extensions: [] + }) + should(pattern.includeFolders).eql(['holidays/venice/', 'holidays/']) + }) + + it('keeps the required include if it ends with a wildcard', () => { + // to ensure sub-sub folders can be traversed as expected + const pattern = new GlobPattern({ + include: ['holidays/venice/**'], + exclude: [], + extensions: [] + }) + should(pattern.includeFolders).eql(['holidays/venice/**', 'holidays/venice/', 'holidays/']) + }) + + it('keeps the required include if it ends with a /', () => { + const pattern = new GlobPattern({ + include: ['holidays/venice/'], + exclude: [], + extensions: [] + }) + should(pattern.includeFolders).eql(['holidays/venice/', 'holidays/']) + }) + + it('combines all include paths (no repetitions)', () => { + const pattern = new GlobPattern({ + include: ['holidays/venice/IMG_001.jpg', 'holidays/milan/IMG_002.jpg'], + exclude: [], + extensions: [] + }) + should(pattern.includeFolders).eql([ + 'holidays/venice/', + 'holidays/', + 'holidays/milan/' + ]) + }) + + it('works with a root wildcard', () => { + const pattern = new GlobPattern({ + include: ['**'], + exclude: [], + extensions: [] + }) + should(pattern.includeFolders).eql(['**']) + }) + + it('works with a root double wildcard', () => { + const pattern = new GlobPattern({ + include: ['**/**'], + exclude: [], + extensions: [] + }) + should(pattern.includeFolders).eql(['**/**', '**/']) + }) + }) + describe('traversing folders', () => { it('traverses folders that meet an include pattern', () => { const pattern = new GlobPattern({ @@ -113,22 +176,58 @@ describe('Index: pattern', function () { should(pattern.canTraverse('holidays')).eql(true) }) - it('traverses nested folders that meet an include pattern', () => { + it('traverses nested folders that meet a deep wildcard (**)', () => { const pattern = new GlobPattern({ include: ['holidays/**', 'home/**'], exclude: [], extensions: [] }) should(pattern.canTraverse('holidays/2016')).eql(true) + should(pattern.canTraverse('holidays/2016/venice')).eql(true) + }) + + it('traverses folders that meet a nested deep wildcard', () => { + const pattern = new GlobPattern({ + include: ['holidays/2016/**', 'home/**'], + exclude: [], + extensions: [] + }) + should(pattern.canTraverse('holidays')).eql(true) + should(pattern.canTraverse('holidays/2016')).eql(true) + should(pattern.canTraverse('holidays/2016/venice')).eql(true) }) - it('traverses folders that meet an include directory', () => { + it('traverses a single folder (no children)', () => { const pattern = new GlobPattern({ include: ['holidays/'], exclude: [], extensions: [] }) should(pattern.canTraverse('holidays')).eql(true) + // only traverses a single level since '/**' wasn't specified + should(pattern.canTraverse('holidays/2016')).eql(false) + }) + + it('traverses a nested folder (no children)', () => { + const pattern = new GlobPattern({ + include: ['holidays/2016/'], + exclude: [], + extensions: [] + }) + should(pattern.canTraverse('holidays')).eql(true) + should(pattern.canTraverse('holidays/2016')).eql(true) + // not beyond since '/**' wasn't specified + should(pattern.canTraverse('holidays/2016/venice')).eql(false) + }) + + it('traverses folders that meet an full-path include pattern', () => { + const pattern = new GlobPattern({ + include: ['holidays/venice/IMG_001.jpg'], + exclude: [], + extensions: [] + }) + should(pattern.canTraverse('holidays')).eql(true) + should(pattern.canTraverse('holidays/venice')).eql(true) }) it('ignores folders that meet an exclude pattern', () => {