UNPKG

alinex-fs

Version:

Extension of nodes filesystem tools.

870 lines (771 loc) 24.6 kB
/* Filter Rules ================================================= The filter is used to select some of the files based on specific settings. You can't call the filter directly but it is used from most methods for file selection. #4 Options The filter definition is given as options array which may have some of the following specification settings. But some methods may have special additional options not mentioned here. The filter is given as `filter` element in the options object and be specified as subobject or list of objects with these settings: - `include` - `Array<String|RegExp>|String|RegExp` to specify a inclusion pattern - `exclude` - `Array<String|RegExp>|String|RegExp` to specify an exclusion pattern - `mindepth` - `Integer` minimal depth to match - `maxdepth` - `Integer` maximal depth to match - `dereference` - `Boolean` set to true to follow symbolic links - `type` - `String` the inode type it should be one of: - `file`, `f` - `directory`, `dir`, `d` - `link`, `l` - `fifo`, `pipe`, `p` - `socket`, `s` - `minsize` - `Integer|String` minimal filesize - `maxsize` - `Integer|String` maximal filesize - `user` - `Integer|String` owner name or id - `group` - `Integer|String` owner group name or id - `accessedAfter` - `Integer|String` last access time should be after that value - `accessedBefore` - `Integer|String` last access time should be before that value - `modifiedAfter` - `Integer|String` last modified time should be after that value - `modifiedBefore` - `Integer|String` last modified time should be before that value - `createdAfter` - `Integer|String` creation time should be after that value - `createdBefore` - `Integer|String` creation time should be before that value - `test` - `Function` own function to use If you use multiple options all of them have to match the file to be valid. See the details below. #4 Multiple Option Sets Multiple sets of the above rules can also be given as list of option arrays. If so all files are allowed, which match any of the given option sets. */ (function() { var async, chrono, debug, filestat, filestatSync, fs, path, posix, sizeHumanToInt, skipDepth, skipDepthSync, skipFunction, skipFunctionSync, skipOwner, skipOwnerSync, skipPath, skipPathSync, skipSize, skipSizeSync, skipTime, skipTimeSync, skipType, skipTypeSync, timeCheck, util; debug = require('debug')('fs:filter'); fs = require('fs'); path = require('path'); async = require('async'); chrono = require('chrono-node'); util = require('util'); posix = require('posix'); module.exports.filter = function(file, depth, options, cb) { var list, subpath; if (depth == null) { depth = 0; } if (options == null) { options = {}; } if (cb == null) { cb = function() {}; } if ((options != null ? options.filter : void 0) == null) { return cb(true); } list = Array.isArray(options.filter) ? options.filter : [options.filter]; subpath = file.split(/\//); subpath = subpath.slice(subpath.length - depth).join('/'); if (!subpath.length) { subpath = null; } return async.map(list, function(options, cb) { if (!Object.keys(options).length) { return cb(true); } return skipPath(subpath != null ? subpath : file, options, function(skip) { if (skip) { if (skip === 'SKIPPATH') { return cb(skip); } return cb(false); } return async.parallel([ function(cb) { return skipDepth(file, depth, options, cb); }, function(cb) { return skipType(file, options, cb); }, function(cb) { return skipSize(file, options, cb); }, function(cb) { return skipTime(file, options, cb); }, function(cb) { return skipOwner(file, options, cb); }, function(cb) { return skipFunction(file, options, cb); } ], function(skip) { return cb(!skip); }); }); }, function(err) { if (err === 'SKIPPATH') { debug(file + " SKIPPATH"); return cb(); } debug(file + " " + (err ? 'OK' : 'SKIP')); return cb(err ? true : false); }); }; module.exports.filterSync = function(file, depth, options) { var i, len, list, res, subpath; if (depth == null) { depth = 0; } if ((options != null ? options.filter : void 0) == null) { return true; } list = Array.isArray(options.filter) ? options.filter : [options.filter]; subpath = file.split(/\//); subpath = subpath.slice(subpath.length - depth).join('/'); if (!subpath.length) { subpath = null; } for (i = 0, len = list.length; i < len; i++) { options = list[i]; if (!Object.keys(options).length) { return true; } debug(("check " + file + " for ") + util.inspect(options)); if (res = skipPathSync(subpath != null ? subpath : file, options)) { if (res === 'SKIPPATH') { debug(file + " SKIP"); return void 0; } continue; } if (skipTypeSync(file, options)) { continue; } if (skipDepthSync(file, depth, options)) { continue; } if (skipSizeSync(file, options)) { continue; } if (skipTimeSync(file, options)) { continue; } if (skipOwnerSync(file, options)) { continue; } if (skipFunctionSync(file, options)) { continue; } debug(file + " OK"); return true; } debug(file + " SKIP"); return false; }; /* #3 File/Path Matching This is based on glob expressions like used in unix systems. You may use these as the `include` or `exclude` pattern while the `exclude` pattern has the higher priority. All files are matched which are in the include pattern and not in the exclude pattern. Both patterns may also be given as `Array` to match multiple. If multiple are given they are combined logically using OR meaning that at least one include have to match and no exclude should match. __Regular expressions__ The pattern may be a regular expression given as String. See {@link RegExp()} for the format description. __Pattern Matching__ Alternatively you may use glob pattern string with the following specification: To use one of the special characters `*`, `?` or `[` you have to preceed it with an backslash. The patter may contain: - `?` (not between brackets) matches any single character. - `*` (not between brackets) matches any string, including the empty string. - `**` (not between brackets) matches any string and also includes the path separator. Character groups: - `[ade]` or `[a-z]` Matches any one of the enclosed characters ranges can be given using a hyphen. - `[!ade]` or `[!a-z]` negates the search and matches any character not enclosed. - `[^ade]` or `[^a-z]` negates the search and matches any character not enclosed. Brace Expansion: - `{a,b}` will be expanded to `a` or `b` - `{a,b{c,d}}` stacked to match `a`, `bc` or `bd` - `{1..3}` will be expanded to `1` or `2` or `3` Extended globbing is also possible: - ?(list): Matches zero or one occurrence of the given patterns. - *(list): Matches zero or more occurrences of the given patterns. - +(list): Matches one or more occurrences of the given patterns. - @(list): Matches one of the given patterns. See more information about pattern matching in {@link minimatch}. __Example__ ``` coffee fs = require 'alinex-fs' fs.find '/tmp/some/directory', filter: include: 'a*' exclude: '*c' , (err, list) -> * list may include 'a', 'abd', 'abe' * but not 'abc' ``` */ skipPath = function(file, options, cb) { return cb(skipPathSync(file, options)); }; skipPathSync = function(file, options) { var exclude, i, include, j, len, len1, list, minimatch, ok; if (!(options.include || options.exclude)) { return false; } if (options.exclude) { list = Array.isArray(options.exclude) ? options.exclude : [options.exclude]; for (i = 0, len = list.length; i < len; i++) { exclude = list[i]; if (exclude instanceof RegExp) { if (file.match(exclude)) { debug("skip " + file + " because path excluded (regexp)"); return 'SKIPPATH'; } } else if (exclude === path.basename(file)) { return true; } else { minimatch = require('minimatch'); if (minimatch(file, exclude, { matchBase: true })) { debug("skip " + file + " because path excluded (glob)"); return 'SKIPPATH'; } } } } if (options.include) { list = Array.isArray(options.include) ? options.include : [options.include]; ok = false; for (j = 0, len1 = list.length; j < len1; j++) { include = list[j]; if (include instanceof RegExp) { if (file.match(include)) { ok = true; break; } } else if (include !== path.basename(file)) { minimatch = require('minimatch'); if (minimatch(file, include, { matchBase: true })) { ok = true; break; } } else { ok = true; break; } } if (!ok) { debug("skip " + file + " because path not included"); return true; } } return false; }; /* #3 Search depth The search depth specifies in which level of subdirectories the filter will match. 1 means everything in the given directory, 2 one level deeper. - `mindepth` - `Integer` minimal depth to match - `maxdepth` - `Integer` maximal depth to match __Example__ ``` coffee fs = require 'alinex-fs' fs.find '/tmp/some/directory', filter: mindepth: 1 maxdepth: 1 , (err, list) -> * only the first sublevele: * list may include 'dir1/abc', 'dir1/abd', 'dir1/abe', 'dir1/bb', 'dir1/bcd', 'dir1/dir2' ``` */ skipDepth = function(file, depth, options, cb) { return cb(skipDepthSync(file, depth, options)); }; skipDepthSync = function(file, depth, options) { if ((options.maxdepth != null) && options.maxdepth < depth) { debug("skip " + file + " because deeper than specified depth"); return 'SKIPPATH'; } if ((options.mindepth != null) && options.mindepth > depth) { debug("skip " + file + " because not in specified depth"); return true; } return false; }; filestat = function(file, options, cb) { var stat; stat = options.dereference != null ? fs.stat : fs.lstat; return stat(file, function(err, stats) { if (err && (options.dereference != null)) { debug("error resolving " + file + " link"); return filestat(file, {}, cb); } return cb(err, stats); }); }; filestatSync = function(file, options) { var error, stat; stat = options.dereference != null ? fs.statSync : fs.lstatSync; try { return stat(file); } catch (error1) { error = error1; debug("error resolving " + file + " link " + error.message); if (options.dereference != null) { return filestatSync(file, {}); } throw error; } }; /* #3 File type Use `type` to specify which type of file you want to use. Possible values: - `file`, `f` - `directory`, `dir`, `d` - `link`, `l` - `fifo`, `pipe`, `p` - `socket`, `s` Also you may set `dereference` to `true` to follow symbolic links and analyze their target. __Example__ ``` coffee fs = require 'alinex-fs' fs.find '/tmp/some/directory', filter: type: 'f' , (err, list) -> * list may include 'test/temp/file1', 'test/temp/file2', 'test/temp/dir1/file11' ``` */ skipType = function(file, options, cb) { if (!options.type) { return cb(); } return filestat(file, options, function(err, stats) { if (err) { debug("skip because error " + err + " in stat for " + file); return cb(); } switch (options.type) { case 'file': case 'f': if (stats.isFile()) { return cb(); } debug("skip " + file + " because not a file entry"); break; case 'directory': case 'dir': case 'd': if (stats.isDirectory()) { return cb(); } debug("skip " + file + " because not a directory entry"); break; case 'link': case 'l': if (stats.isSymbolicLink()) { return cb(); } debug("skip " + file + " because not a link entry"); break; case 'fifo': case 'pipe': case 'p': if (stats.isFIFO()) { return cb(); } debug("skip " + file + " because not a FIFO entry"); break; case 'socket': case 's': if (stats.isSocket()) { return cb(); } debug("skip " + file + " because not a socket entry"); } return cb(true); }); }; skipTypeSync = function(file, options) { var err, stats; if (!options.type) { return false; } try { stats = filestatSync(file, options); } catch (error1) { err = error1; debug("skip because error " + err + " in stat for " + file); return; } switch (options.type) { case 'file': case 'f': if (stats.isFile()) { return; } debug("skip " + file + " because not a file entry"); break; case 'directory': case 'dir': case 'd': if (stats.isDirectory()) { return; } debug("skip " + file + " because not a directory entry"); break; case 'link': case 'l': if (stats.isSymbolicLink()) { return; } debug("skip " + file + " because not a link entry"); break; case 'fifo': case 'pipe': case 'p': if (stats.isFIFO()) { return; } debug("skip " + file + " because not a FIFO entry"); break; case 'socket': case 's': if (stats.isSocket()) { return; } debug("skip " + file + " because not a socket entry"); } return true; }; sizeHumanToInt = function(text) { var match; if (typeof text === 'string' && (match = text.match(/^(\d*\.?\d*)\s*([kKmMgGtTpP])$/))) { switch (match[2]) { case 'k': case 'K': return match[1] * 1024; case 'm': case 'M': return match[1] * Math.pow(1024, 2); case 'g': case 'G': return match[1] * Math.pow(1024, 3); case 'T': case 'T': return match[1] * Math.pow(1024, 4); case 'P': case 'P': return match[1] * Math.pow(1024, 5); } } return text; }; /* #3 File size With the `minsize` and `maxsize` options it is possible to specify the exact size of the matching files in bytes: - use an `Integer` value as number of bytes - use a `String` like `1M` or `100k` __Example__ ``` coffee fs = require 'alinex-fs' fs.find '/tmp/some/directory', filter: maxsize: 1024 * 1024 , (err, list) -> * list contains only files larger than 1MB ``` */ skipSize = function(file, options, cb) { if (!(options.minsize || options.maxsize)) { return cb(); } if (options.minsize) { options.minsize = sizeHumanToInt(options.minsize); } if (options.maxsize) { options.maxsize = sizeHumanToInt(options.maxsize); } return filestat(file, options, function(err, stats) { var skip; if (err) { debug("skip because error " + err + " in stat for " + file); return cb(); } skip = ((options.minsize != null) && options.minsize > stats.size) || ((options.maxsize != null) && options.maxsize < stats.size); if (skip) { debug("skip " + file + " because size mismatch"); } return cb(skip); }); }; skipSizeSync = function(file, options) { var err, skip, stats; if (!(options.minsize || options.maxsize)) { return false; } if (options.minsize) { options.minsize = sizeHumanToInt(options.minsize); } if (options.maxsize) { options.maxsize = sizeHumanToInt(options.maxsize); } try { stats = filestatSync(file, options); } catch (error1) { err = error1; debug("skip because error " + err + " in stat for " + file); return; } skip = ((options.minsize != null) && options.minsize > stats.size) || ((options.maxsize != null) && options.maxsize < stats.size); if (skip) { debug("skip " + file + " because size mismatch"); } return skip; }; /* #3 Owner and Group You may also specify files based on the user which owns the files or the group of the files. Both may be specified as id (uid or gid) or using the alias name. - `user` - `Integer|String` owner name or id - `group` - `Integer|String` owner group name or id __Example__ ``` coffee fs = require 'alinex-fs' fs.find '/tmp/some/directory', filter: user: process.uid , (err, list) -> * list contains only files belonging to the current user ``` */ skipOwner = function(file, options, cb) { var error, gid, uid; if (!(options.user || options.group)) { return cb(); } if (options.user && !isNaN(options.user)) { try { uid = posix.getpwnam(options.user).uid; } catch (error1) { error = error1; return cb(error); } } if (options.group && !isNaN(options.group)) { try { gid = posix.getgrnam(options.group).gid; } catch (error1) { error = error1; return cb(error); } } return filestat(file, options, function(err, stats) { var skip; if (err) { if (err) { return cb(err); } debug("skip because error " + err + " in stat for " + file); return cb(); } skip = (uid && uid === !stats.uid) || (gid && gid === !stats.gid); if (skip) { debug("skip " + file + " because owner mismatch"); } return cb(skip); }); }; skipOwnerSync = function(file, options) { var err, gid, skip, stats, uid; if (!(options.user || options.group)) { return false; } if (options.user && !isNaN(options.user)) { uid = posix.getpwnam(options.user).uid; } if (options.group && !isNaN(options.group)) { gid = posix.getgrnam(options.group).gid; } try { stats = filestatSync(file, options); } catch (error1) { err = error1; debug("skip because error " + err + " in stat for " + file); throw err; } skip = (uid && uid === !stats.uid) || (gid && gid === !stats.gid); if (skip) { debug("skip " + file + " because owner mismatch"); } return skip; }; timeCheck = function(stats, options) { var dir, i, j, len, len1, ref, ref1, ref2, ref3, type, value; ref1 = ['accessed', 'modified', 'created']; for (i = 0, len = ref1.length; i < len; i++) { type = ref1[i]; ref2 = ['After', 'Before']; for (j = 0, len1 = ref2.length; j < len1; j++) { dir = ref2[j]; if (!options[type + dir]) { continue; } ref = options[type + dir]; if (typeof ref === 'string') { ref = ((ref3 = chrono.parseDate(ref)) != null ? ref3.getTime() : void 0) / 1000; } if (!ref) { throw new Error("Given value '" + options[type + dir] + "' in option " + (type + dir) + " is invalid."); } value = stats[type.charAt(0) + 'time'].getTime() / 1000; if (dir === 'Before' && value >= ref) { return false; } if (dir === 'After' && value <= ref) { return false; } } } return true; }; /* #3 Time specification It is also possible to select files based on their `creation`, last `modified` or last `accessed` time. Specify the `Before` and `After` time appended to one of the above as: - Unix timestamp - ISO-8601 date formats - some local formats (based on platform support for Date.parse()) - time difference from now (human readable) The following time definitions are an example what you may use: - `yesterday`, `2 days ago`, `last Monday` to specify a day from now - `yesterday 15:00`, `yesterday at 15:00` to also specify the time - `1 March`, `1st March` specifies a date in this year - `1 March 2014`, `1st March 2014`, '03/01/13`, `01.03.2014` all specifiying the 1st of march - `9:00`, `9:00 GMT+0900` to specify a time today or in combination with a date - `last night`, `00:00` If only a day is given it will use 12:00 as the time. __Example__ ``` coffee fs = require 'alinex-fs' fs.find '/tmp/some/directory', filter: modifiedBefore: 'yesterday 12:00' , (err, list) -> * list contains only files older than yesterday 12 o'clock ``` */ skipTime = function(file, options, cb) { var dir, i, j, len, len1, ref1, ref2, type, used; used = false; ref1 = ['accessed', 'modified', 'created']; for (i = 0, len = ref1.length; i < len; i++) { type = ref1[i]; ref2 = ['After', 'Before']; for (j = 0, len1 = ref2.length; j < len1; j++) { dir = ref2[j]; if (options[type + dir]) { used = true; } } } if (!used) { return cb(false); } return filestat(file, options, function(err, stats) { var skip; if (err) { debug("skip because error " + err + " in stat for " + file); return cb(); } skip = !timeCheck(stats, options); if (skip) { debug("skip " + file + " because out of time range"); } return cb(skip); }); }; skipTimeSync = function(file, options) { var dir, err, i, j, len, len1, ref1, ref2, skip, stats, type, used; used = false; ref1 = ['accessed', 'modified', 'created']; for (i = 0, len = ref1.length; i < len; i++) { type = ref1[i]; ref2 = ['After', 'Before']; for (j = 0, len1 = ref2.length; j < len1; j++) { dir = ref2[j]; if (options[type + dir]) { used = true; } } } if (!used) { return false; } try { stats = filestatSync(file, options); } catch (error1) { err = error1; debug("skip because error " + err + " in stat for " + file); return; } skip = !timeCheck(stats, options); if (skip) { debug("skip " + file + " because out of time range"); } return skip; }; /* #3 User defined function With the `test` parameter you may add an user defined function which will be called to check each file. It will get the file path and options array so you may also add some configuration therefore in additional option values. Asynchronous call: ``` coffee fs = require 'alinex-fs' fs.find '.', filter: test: (file, options, cb) -> cb ~file.indexOf 'ab' , (err, list) -> console.log "Found #{list.length} matches." ``` Or use synchronous calls: ``` coffee fs = require 'alinex-fs' list = fs.findSync 'test/temp', filter: test: (file, options) -> return ~file.indexOf 'ab' console.log "Found #{list.length} matches." ``` */ skipFunction = function(file, options, cb) { if (!(options.test || typeof options.test === !'function')) { return cb(); } return options.test(file, options, function(ok) { if (!ok) { debug("skip " + file + " by user function"); } return cb(!ok); }); }; skipFunctionSync = function(file, options) { var ok; if (!(options.test || typeof options.test === !'function')) { return false; } ok = options.test(file, options); if (!ok) { debug("skip " + file + " by user function"); } return !ok; }; }).call(this); //# sourceMappingURL=filter.map