/
index.ts
557 lines (521 loc) · 19.7 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
/**
* @packageDocumentation
* project: recursive-readdir-async
* @author: m0rtadelo (ricard.figuls)
* @license MIT
* 2018
*/
/**
* A fs.Stats object provides information about a file.
* @external fs.Stats
* @see https://nodejs.org/api/fs.html#fs_classfs_stats
*/
/**
* Options/Settings options available for this module
* @typedef Options
* @type {object}
* @property [mode] - The list will return an array of items. The tree will return the
* items structured like the file system. Default: LIST
* @property [recursive] - If true, files and folders of folders and subfolders will be listed.
* If false, only the files and folders of the select directory will be listed. Default: true
* @property [stats] - If true a stats object (with file information) will be added to every item.
* If false this info is not added. Default: false.
* @property [ignoreFolders] - If true and mode is LIST, the list will be returned with files only.
* If true and mode is TREE, the directory structures without files will be deleted.
* If false, all empty and non empty directories will be listed. Default: true
* @property [extensions] - If true, lowercase extensions will be added to every item in the extension object property
* (file.TXT => info.extension = ".txt"). Default: false
* @property [deep] - If true, folder depth information will be added to every item starting with 0 (initial path),
* and will be incremented by 1 in every subfolder. Default: false
* @property [realPath] - Computes the canonical pathname by resolving ., .. and symbolic links. Default: true
* @property [normalizePath] - Normalizes windows style paths by replacing double backslahes with single forward
* slahes (unix style). Default: true
* @property [include] - Positive filter the items: only items which DO (partially or completely) match one of the
* strings in the include array will be returned. Default: []
* @property [exclude] - Negative filter the items: only items which DO NOT (partially or completely) match any of
* the strings in the exclude array will be returned. Default: []
* @property [readContent] - Adds the content of the file into the item (base64 format). Default: false
* @property [encoding] - Sets the encoding format to use in the readFile FS native node function
* (ascii, base64, binary, hex, ucs2/ucs-2/utf16le/utf-16le, utf8/utf-8, latin1). Default: 'base64'
*/
export interface IOptions {
/** The list will return an array of items. The tree will return the items structured like the file system.
* Default: LIST */
mode?: any,
/** If true, files and folders of folders and subfolders will be listed. If false, only the files and folders
* of the select directory will be listed. Default: true */
recursive?: boolean,
/** If true a stats object (with file information) will be added to every item. If false this info is not added.
* Default: false. */
stats?: any,
/** If true and mode is LIST, the list will be returned with files only. If true and mode is TREE, the directory
* structures without files will be deleted. If false, all empty and non empty directories will be listed.
* Default: true */
ignoreFolders?: boolean,
/** If true, lowercase extensions will be added to every item in the extension object property
* (file.TXT => info.extension = ".txt"). Default: false */
extensions?: boolean,
/** If true, folder depth information will be added to every item starting with 0 (initial path), and will be
* incremented by 1 in every subfolder. Default: false */
deep?: boolean,
/** Computes the canonical pathname by resolving ., .. and symbolic links. Default: true */
realPath?: boolean,
/** Normalizes windows style paths by replacing double backslahes with single forward
* slahes (unix style). Default: true */
normalizePath?: boolean,
/** Positive filter the items: only items which DO (partially or completely) match one of the
* strings in the include array will be returned. Default: [] */
include?: string[],
/** Negative filter the items: only items which DO NOT (partially or completely) match any of the
* strings in the exclude array will be returned. Default: [] */
exclude?: string[],
/** Adds the content of the file into the item (base64 format). Default: false */
readContent?: boolean,
/** Sets the encoding format to use in the readFile FS native node function (ascii, base64, binary, hex,
* ucs2/ucs-2/utf16le/utf-16le, utf8/utf-8, latin1). Default: 'base64' */
encoding?: BufferEncoding,
}
/**
* Definition for the common dto object that contains information of the files and folders
* @typedef IBase
* @type {object}
* @property name - The filename of the file
* @property title - The title of the file (no extension)
* @property path - The path of the item
* @property fullname - The fullname of the file (path & name & extension)
* @property extension - The extension of the file with dot in lowercase
* @property deep - The depth of current content
* @property isDirectory - True for directory, false for files
* @property error - The error object. The structure is variable
* @property custom - Custom key to add custom properties
*/
export interface IBase {
/** The filename of the file */
name: string,
/** The filename of the file (buffer version) */
nameb: Buffer,
/** The title of the file (no extension) */
title: string,
/** The path of the file */
path: string,
/** The path of the file (buffer version) */
pathb: Buffer,
/** The fullname of the file (path & name & extension) */
fullname: string,
/** The fullname of the file (path & name & extension buffer version) */
fullnameb: Buffer,
/** The extension of the file with dot in lowercase */
extension?: string,
/** The depth of current content */
deep?: number,
/** True for directory, false for files */
isDirectory?: boolean,
/** If something goes wrong the error comes here */
error?: IError|any,
/** Custom key to add custom properties */
custom?: any,
}
/**
* Definition for the main Error object that contains information of the current exception
* @typedef IError
* @type {object}
* @property error - The error object. The structure is variable
* @property path - The path where the error is related to
*/
export interface IError {
/** The raw error returned by service */
error: any,
/** Path where the error raises exception */
path: string,
[index:number]: any,
}
/**
* Definition for the Item object that contains information of files used in this module
* @typedef IFile
* @type {object}
* @property name - The filename of the file
* @property path - The path of the file
* @property title - The title of the file (no extension)
* @property fullname - The fullname of the file (path & name)
* @property [extension] - The extension of the file in lowercase
* @property [isDirectory] - Always false in files
* @property [data] - The content of the file in a base64 string by default
* @property [stats] - The stats (information) of the file
* @property [error] - If something goes wrong the error comes here
* @property [deep] - The depth of current content
*/
export interface IFile extends IBase {
/** The content of the file in a base64 string by default */
data?: string,
/** The stats (information) of the file */
stats?: _fs.Stats,
}
/**
* Definition for the Item object that contains information of folders used but this module
* @typedef IFolder
* @type {object}
* @property name - The filename of the folder
* @property path - The path of the folder
* @property title - The title of the file (no extension)
* @property fullname - The fullname of the folder (path & name)
* @property [extension] - The extension of the folder in lowercase
* @property [isDirectory] - Always true in folders
* @property [content] - Array of File/Folder content
* @property [error] - If something goes wrong the error comes here
* @property [deep] - The depth of current content
*/
export interface IFolder extends IBase {
/** The content of the Folder (if any) */
content?:(IFile|IFolder)[]|IError,
}
/**
* @typedef CallbackFunction
* @type {function}
* @param item - The item object with all the required fields
* @param index - The current index in the array/collection of Files and/or Folders
* @param total - The total number of Files and/or Folders
* @returns {boolean} - true to delete the item from the list
*/
export interface ICallback {
/** The item object with all the required fields */
item: IFile|IFolder,
/** The current index in the array/collection of Files and/or Folders */
index: number,
/** The total number of Files and/or Folders */
total: number,
}
/** @readonly constant for mode LIST to be used in Options */
export const LIST = 1;
/** @readonly constant for mode TREE to be used in Options */
export const TREE = 2;
/**
* native FS module
* @see https://nodejs.org/api/fs.html#fs_file_system
* @external
*/
import * as _fs from 'fs';
/** native node fs object */
export const FS = _fs;
/**
* native PATH module
* @external
* @see https://nodejs.org/api/path.html#path_path
*/
import * as _path from 'path';
/** native node path object */
export const PATH = _path;
let pathSimbol = '/';
/**
* Returns a Promise with Stats info of the item (file/folder/...)
* @param file the name of the object to get stats from
* @returns {Promise<fs.Stats>} stat object information
* @async
*/
export async function stat(buffer:Buffer): Promise<_fs.Stats> {
return new Promise(function(resolve, reject) {
FS.stat(buffer, function(err: any, stats: _fs.Stats) {
if (err) {
reject(err);
} else {
resolve(stats);
}
});
});
}
/**
* Returns a Promise with content (data) of the file
* @param file the name of the file to read content from
* @param encoding format for returned data (ascii, base64, binary, hex, ucs2/ucs-2/utf16le/utf-16le,
* utf8/utf-8, latin1). Default: base64
* @returns {Promise<string>} data content string (base64 format by default)
* @async
*/
export async function readFile(file: Buffer, encoding: BufferEncoding|undefined = 'base64'): Promise<string> {
return new Promise(function(resolve, reject) {
FS.readFile(file, { encoding }, function(err: any, data: string) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
/**
* Returns if an item should be added based on include/exclude options.
* @param path the item fullpath
* @param settings the options configuration to use
* @returns {boolean} if item must be added
* @private
*/
function checkItem(path: string, settings: IOptions): boolean {
if (settings.exclude) {
for (const value of settings.exclude) {
if (path.includes(value)) {
return false;
}
}
}
return true;
}
/**
* Adds optional keys to item
* @param obj the item object
* @param file the filename
* @param settings the options configuration to use
* @param deep The deep level
* @returns void
* @private
*/
function addOptionalKeys(obj:IBase, file: string, settings: IOptions, deep: number) {
if (settings.extensions) {
obj.extension = (PATH.extname(file)).toLowerCase();
}
if (settings.deep) {
obj.deep = deep;
}
}
/**
* Reads content and creates a valid IBase collection
* @param rpath Path relative to
* @param data Model
* @param settings the options configuration to use
* @param deep The deep level
* @param resolve Promise
* @param reject Promise
* @returns void
*/
function read(rpath: string, data: any, settings: IOptions, deep: number, resolve: any, reject: any) {
FS.readdir(rpath, 'buffer', function(error: any, files: Buffer[]) {
// If error reject them
if (error) {
reject(error);
} else {
const removeExt = (file: string) => {
const extSize = PATH.extname(file).length;
return file.substring(0, file.length - (extSize > 0 ? extSize : 0));
};
// Iterate through elements (files and folders)
for (const file of files) {
const path = rpath + (rpath.endsWith(pathSimbol) ? '' : pathSimbol);
const obj:IBase = {
name: file.toString(),
nameb: file,
title: removeExt(file.toString()),
path: rpath,
pathb: Buffer.from(rpath),
fullname: path + file.toString(),
fullnameb: Buffer.concat([Buffer.from(path), file]),
};
if (checkItem(obj.fullname, settings)) {
addOptionalKeys(obj, file.toString(), settings, deep);
data.push(obj);
}
}
// Finish, returning content
resolve(data);
}
});
}
/**
* Returns a Promise with an objects info array
* @param path the item fullpath to be searched for
* @param settings the options configuration to use
* @param deep folder depth value
* @returns {Promise<IBase[]>} the file object info
* @private
*/
async function myReaddir(path: string, settings: IOptions, deep: number): Promise<IBase[]> {
const data:IBase[] = [];
return new Promise(function(resolve, reject) {
try {
// Asynchronously computes the canonical pathname by resolving ., .. and symbolic links.
FS.realpath(path, function(err: any, rpath: string) {
if (err || settings.realPath === false) {
rpath = path;
}
// Normalizes windows style paths by replacing double backslahes with single forward slahes (unix style).
if (settings.normalizePath) {
rpath = normalizePath(rpath);
}
// Reading contents of path
read(rpath, data, settings, deep, resolve, reject);
});
} catch (err) {
// If error reject them
reject(err);
}
});
}
/**
* Normalizes windows style paths by replacing double backslahes with single forward slahes (unix style).
* @param path windows/unix path
* @return {string} normalized path (unix style)
* @private
*/
function normalizePath(path: string): string {
return path.toString().replace(/\\/g, '/');
}
/**
* Search if the fullname exist in the include array
* @param fullname - The fullname of the item to search for
* @param settings the options to be used
* @returns true if exists
*/
function exists(fullname: string, settings: IOptions): boolean {
if (settings.include) {
for (const value of settings.include) {
if (fullname.includes(value)) {
return true;
}
}
}
return false;
}
/**
* Removes paths that not match the include array
* @param settings the options to be used
* @param content items list
* @returns void
*/
function onlyInclude(settings: IOptions, content: (IFile|IFolder)[]) {
if (settings.include && settings.include.length > 0) {
for (let i = content.length - 1; i > -1; i--) {
const item = content[i];
if (settings.mode === TREE && item.isDirectory && (item as IFolder).content) continue;
if (!exists(item.fullname, settings)) {
content.splice(i, 1);
}
}
}
}
/**
* Returns an array of items in path
* @param path path
* @param settings the options to be used
* @param progress callback progress
* @param deep deep index information
* @returns {object[]} array with file information
* @private
*/
async function listDir(
path: string, settings: IOptions, progress:Function|undefined, deep = 0,
): Promise<(IFile|IFolder)[]|IError> {
let content: (IFile|IFolder)[];
try {
content = await myReaddir(path, settings, deep);
} catch (err) {
return { 'error': err, 'path': path };
}
if (settings.stats || settings.recursive || !settings.ignoreFolders ||
settings.readContent || settings.mode === TREE) {
content = await statDir(content, settings, progress, deep);
}
onlyInclude(settings, content);
return content;
}
/**
* Returns an object with all items with selected options
* @param collection items list
* @param settings the options to use
* @param progress callback progress
* @param deep folder depth
* @returns {object[]} array with file information
* @private
*/
async function statDir(
collection:(IFile|IFolder)[], settings: IOptions, progress: Function|undefined, deep: number,
): Promise<(IFile|IFolder)[]> {
let isOk = true;
for (let i = collection.length - 1; i > -1; i--) {
try {
collection = await statDirItem(collection, i, settings, progress, deep);
if (progress !== undefined) {
isOk = !progress(collection[i], collection.length - i, collection.length);
}
} catch (err) {
collection[i].error = err;
}
if ((collection[i].isDirectory && settings.ignoreFolders &&
!((collection[i] as IFolder).content) && collection[i].error === undefined) || !isOk) {
collection.splice(i, 1);
}
}
return collection;
}
/**
* Returns an object with updated item information
* @param collection items list
* @param i index of item
* @param settings the options to use
* @param progress callback progress
* @param deep folder depth
* @returns {object[]} array with file information
* @private
*/
async function statDirItem(
collection:(IFile|IFolder)[], i: number, settings: IOptions, progress: Function|undefined, deep: number,
):Promise<(IFile|IFolder)[]> {
const stats = await stat(collection[i].fullnameb);
collection[i].isDirectory = stats.isDirectory();
if (settings.stats) {
(collection[i] as IFile).stats = stats;
}
if (settings.readContent && !collection[i].isDirectory) {
(collection[i] as IFile).data = await readFile(collection[i].fullnameb, settings.encoding);
}
if (collection[i].isDirectory && settings.recursive) {
const item: IFolder = collection[i];
if (settings.mode === LIST) {
const result: (IFile|IFolder)[]|IError|any = await listDir(item.fullname, settings, progress, deep + 1);
if (result.length) {
collection = collection.concat(result);
}
} else {
item.content = await listDir(item.fullname, settings, progress, deep + 1);
if (item.content && (item.content as IFolder[]).length === 0) {
item.content = undefined;
}
}
}
return collection;
}
/**
* Returns a javascript object with directory items information (non blocking async with Promises)
* @param path the path to start reading contents
* @param [options] options (mode, recursive, stats, ignoreFolders)
* @param [progress] callback with item data and progress info for each item
* @returns promise array with file/folder information
* @async
*/
export async function list(
path: string, options?: IOptions|Function, progress?:Function,
): Promise<(IFile|IFolder)[]|IError|any> {
// options skipped?
if (typeof options === 'function') {
progress = options;
}
// Setting default settings
const settings = {
mode: (options as IOptions)?.mode || LIST,
recursive: (options as IOptions)?.recursive === undefined ? true : (options as IOptions).recursive,
stats: (options as IOptions)?.stats === undefined ? false : (options as IOptions).stats,
ignoreFolders: (options as IOptions)?.ignoreFolders === undefined ? true : (options as IOptions).ignoreFolders,
extensions: (options as IOptions)?.extensions === undefined ? false : (options as IOptions).extensions,
deep: (options as IOptions)?.deep === undefined ? false : (options as IOptions).deep,
realPath: (options as IOptions)?.realPath === undefined ? true : (options as IOptions).realPath,
normalizePath: (options as IOptions)?.normalizePath === undefined ? true : (options as IOptions).normalizePath,
include: (options as IOptions)?.include || [],
exclude: (options as IOptions)?.exclude || [],
readContent: (options as IOptions)?.readContent === undefined ? false : (options as IOptions).readContent,
encoding: (options as IOptions)?.encoding || undefined,
};
// Setting pathSimbol if normalizePath is disabled
if (settings.normalizePath === false) {
pathSimbol = PATH.sep;
} else {
pathSimbol = '/';
}
// Reading contents
return listDir(path, settings, progress);
}