@cumulus/ingest
Version:
Ingest utilities
91 lines (86 loc) • 3.67 kB
text/typescript
import path from 'path';
import * as log from '@cumulus/common/log';
export interface RecursionFile {
type: number | string,
name: string
}
/**
* Recur on directory, list all files, and recur into any further directories,
* as specified regex segments allow.
*
* @param {Function} fn - list function
* @param {string} currentPath - current path to list
* @param {Array<string>} segments - path segments
* @param {number} position - current position in the segment list
* @returns {Array<Object>} - filtered contents of directory
*/
async function recurOnDirectory<T extends RecursionFile>(
fn: (path: string) => Promise<T[]>,
currentPath: string,
segments: string[],
position: number
): Promise<T[]> {
// interpret the next path segment as a regex for filtering, and
// recursively list everything when we've run out of segments
const filterExpr = segments[position + 1] || '.*';
const filterRegex = new RegExp(filterExpr);
const contents = await fn(currentPath);
let files: T[] = [];
for (let ctr = 0; ctr < contents.length; ctr += 1) {
const item = contents[ctr];
// check if item passes filter
if (filterRegex.test(item.name)) {
if (['-', 0].includes(item.type)) {
files.push(item);
} else if (['d', 1].includes(item.type)) {
const nextDir = path.normalize(`${currentPath}/${item.name}`);
files = files.concat(
// eslint-disable-next-line no-await-in-loop
await recurOnDirectory(fn, nextDir, segments, position + 1)
);
}
}
}
return files;
}
/**
* Handles recursion of a FTP/SFTP list operation
* It requests a promisified list function that returns contents of
* a directory on a server, filtering on provided regex segments.
*
* Note that calls to the list function will use either a relative or absolute path, corresponding
* to the `configuredPath` passed into this function. The list function will initially be called
* with '.' for a relative path or '/' for an absolute path. List functions will need to be able to
* normalize or correct these paths as appropriate for their protocol.
*
* Further calls to the list functions will append the current path to that starting path, such
* that all calls will start with either '.' or '/', regardless of additional characters, e.g.
* `fn('./path')` vs. `fn('path')`.
*
* In the case of failure during the recursive list, this function will only apply `path.normalize`
* to the `configuredPath` and then call the list function with the entire normalizedPath.
*
* @param {function} fn - the promisified function for listing a directory
* @param {string} configuredPath - path string configured by operator, which may contain
* regexes for filtering
* @returns {Promise} the promise of an object that has the path is the key and
* list of files as values
*/
export async function recursion<T extends RecursionFile>(
fn: (path: string) => Promise<T[]>,
configuredPath: string
): Promise<T[]> {
const normalizedPath = path.normalize(configuredPath);
const isAbsolutePath = path.isAbsolute(normalizedPath);
try {
const segments = normalizedPath
.split('/') // split on divider
.filter((segment) => segment.trim() !== ''); // filter out empty strings from split
const startingPath = isAbsolutePath ? '/' : '.';
return await recurOnDirectory<T>(fn, startingPath, segments, -1);
} catch (error) {
log.error(`Encountered error during recursive list filtering: ${error}`);
log.info('Falling back to unfiltered directory listing...');
return recurOnDirectory(fn, normalizedPath, [], 0);
}
}