UNPKG

19.4 kBJavaScriptView Raw
1'use strict';
2
3/**
4 * Defines functions to interact with the file system as an extension to the Node.js filesystem module.
5 *
6 * // Load module "fileSystem"
7 * var fsApi = require('@openveo/api').fileSystem;
8 *
9 * @module fileSystem
10 * @main fileSystem
11 * @class fileSystem
12 * @static
13 */
14
15var fs = require('fs');
16var path = require('path');
17var tar = require('tar-fs');
18
19/**
20 * Creates a directory recursively and asynchronously.
21 *
22 * If parent directories do not exist, they will be automatically created.
23 *
24 * @method mkdirRecursive
25 * @private
26 * @static
27 * @async
28 * @param {String} directoryPath The directory system path to create
29 * @param {Function} callback The function to call when done
30 * - **Error** The error if an error occurred, null otherwise
31 */
32function mkdirRecursive(directoryPath, callback) {
33 directoryPath = path.resolve(directoryPath);
34
35 // Try to create directory
36 fs.mkdir(directoryPath, function(error) {
37
38 if (error && error.code === 'EEXIST') {
39
40 // Can't create directory it already exists
41 // It may have been created by another loop
42 callback();
43
44 } else if (error && error.code === 'ENOENT') {
45
46 // Can't create directory, parent directory does not exist
47
48 // Create parent directory
49 mkdirRecursive(path.dirname(directoryPath), function(error) {
50 if (!error) {
51
52 // Now that parent directory is created, create requested directory
53 fs.mkdir(directoryPath, function(error) {
54 if (error && error.code === 'EEXIST') {
55
56 // Can't create directory it already exists
57 // It may have been created by another loop
58 callback();
59
60 } else
61 callback(error);
62 });
63
64 } else
65 callback(error);
66 });
67 } else
68 callback(error);
69 });
70}
71
72/**
73 * Removes a directory and all its content recursively and asynchronously.
74 *
75 * It is assumed that the directory exists.
76 *
77 * @method rmdirRecursive
78 * @private
79 * @static
80 * @async
81 * @param {String} directoryPath Path of the directory to remove
82 * @param {Function} callback The function to call when done
83 * - **Error** The error if an error occurred, null otherwise
84 */
85function rmdirRecursive(directoryPath, callback) {
86
87 // Open directory
88 fs.readdir(directoryPath, function(error, resources) {
89
90 // Failed reading directory
91 if (error)
92 return callback(error);
93
94 var pendingResourceNumber = resources.length;
95
96 // No more pending resources, done for this directory
97 if (!pendingResourceNumber) {
98
99 // Remove directory
100 fs.rmdir(directoryPath, callback);
101
102 }
103
104 // Iterate through the list of resources in the directory
105 resources.forEach(function(resource) {
106
107 var resourcePath = path.join(directoryPath, resource);
108
109 // Get resource stats
110 fs.stat(resourcePath, function(error, stats) {
111 if (error)
112 return callback(error);
113
114 // Resource correspond to a directory
115 if (stats.isDirectory()) {
116
117 resources = rmdirRecursive(path.join(directoryPath, resource), function(error) {
118 if (error)
119 return callback(error);
120
121 pendingResourceNumber--;
122
123 if (!pendingResourceNumber)
124 fs.rmdir(directoryPath, callback);
125
126 });
127
128 } else {
129
130 // Resource does not correspond to a directory
131 // Mark resource as treated
132
133 // Remove file
134 fs.unlink(resourcePath, function(error) {
135 if (error)
136 return callback(error);
137 else {
138 pendingResourceNumber--;
139
140 if (!pendingResourceNumber)
141 fs.rmdir(directoryPath, callback);
142
143 }
144 });
145
146 }
147
148 });
149
150 });
151
152 });
153
154}
155
156/**
157 * Reads a directory content recursively and asynchronously.
158 *
159 * It is assumed that the directory exists.
160 *
161 * @method readdirRecursive
162 * @private
163 * @static
164 * @async
165 * @param {String} directoryPath Path of the directory
166 * @param {Function} callback The function to call when done
167 * - **Error** The error if an error occurred, null otherwise
168 * - **Array** The list of fs.Stats corresponding to resources inside the directory (files and directories)
169 */
170function readdirRecursive(directoryPath, callback) {
171 var resources = [];
172
173 // Read directory
174 fs.readdir(directoryPath, function(error, resourcesNames) {
175
176 // Failed reading directory
177 if (error) return callback(error);
178
179 var pendingResourceNumber = resourcesNames.length;
180
181 // No more pending resources, done for this directory
182 if (!pendingResourceNumber)
183 callback(null, resources);
184
185 // Iterate through the list of resources in the directory
186 resourcesNames.forEach(function(resourceName) {
187 var resourcePath = path.join(directoryPath, resourceName);
188
189 // Get resource stats
190 fs.stat(resourcePath, function(error, stats) {
191 if (error)
192 return callback(error);
193
194 stats.path = resourcePath;
195 resources.push(stats);
196
197 // Resource correspond to a directory
198 if (stats.isDirectory()) {
199
200 readdirRecursive(resourcePath, function(error, paths) {
201 if (error)
202 return callback(error);
203
204 resources = resources.concat(paths);
205 pendingResourceNumber--;
206
207 if (!pendingResourceNumber)
208 callback(null, resources);
209
210 });
211
212 } else {
213
214 // Resource does not correspond to a directory
215 // Mark resource as treated
216
217 pendingResourceNumber--;
218
219 if (!pendingResourceNumber)
220 callback(null, resources);
221 }
222
223 });
224
225 });
226
227 });
228
229}
230
231/**
232 * Copies a file.
233 *
234 * If directory does not exist it will be automatically created.
235 *
236 * @method copyFile
237 * @private
238 * @static
239 * @async
240 * @param {String} sourceFilePath Path of the file
241 * @param {String} destinationFilePath Final path of the file
242 * @param {Function} callback The function to call when done
243 * - **Error** The error if an error occurred, null otherwise
244 */
245function copyFile(sourceFilePath, destinationFilePath, callback) {
246 var onError = function(error) {
247 callback(error);
248 };
249
250 var safecopy = function(sourceFilePath, destinationFilePath, callback) {
251 if (sourceFilePath && destinationFilePath && callback) {
252 try {
253 var is = fs.createReadStream(sourceFilePath);
254 var os = fs.createWriteStream(destinationFilePath);
255
256 is.on('error', onError);
257 os.on('error', onError);
258
259 is.on('end', function() {
260 os.end();
261 });
262
263 os.on('finish', function() {
264 callback();
265 });
266
267 is.pipe(os);
268 } catch (e) {
269 callback(new Error(e.message));
270 }
271 } else callback(new Error('File path not defined'));
272 };
273
274 var pathDir = path.dirname(destinationFilePath);
275
276 this.mkdir(pathDir,
277 function(error) {
278 if (error) callback(error);
279 else safecopy(sourceFilePath, destinationFilePath, callback);
280 }
281 );
282}
283
284/**
285 * The list of file types.
286 *
287 * @property FILE_TYPES
288 * @type Object
289 * @final
290 */
291module.exports.FILE_TYPES = {
292 JPG: 'jpg',
293 PNG: 'png',
294 GIF: 'gif',
295 TAR: 'tar',
296 MP4: 'mp4',
297 BMP: 'bmp',
298 UNKNOWN: 'unknown'
299};
300
301Object.freeze(this.FILE_TYPES);
302
303/**
304 * The list of file types.
305 *
306 * @property FILE_TYPES
307 * @type Object
308 * @final
309 */
310module.exports.FILE_SIGNATURES = {
311 [this.FILE_TYPES.JPG]: [
312 {
313 offset: 0,
314 signature: 'ffd8ffdb'
315 },
316 {
317 offset: 0,
318 signature: 'ffd8ffe0'
319 },
320 {
321 offset: 0,
322 signature: 'ffd8ffe1'
323 },
324 {
325 offset: 0,
326 signature: 'ffd8fffe'
327 }
328 ],
329 [this.FILE_TYPES.PNG]: [{
330 offset: 0,
331 signature: '89504e47'
332 }],
333 [this.FILE_TYPES.GIF]: [{
334 offset: 0,
335 signature: '47494638'
336 }],
337 [this.FILE_TYPES.TAR]: [{
338 offset: 257,
339 signature: '7573746172' // ustar
340 }],
341 [this.FILE_TYPES.MP4]: [
342 {
343 offset: 4,
344 signature: '6674797069736f6d' // isom
345 },
346 {
347 offset: 4,
348 signature: '6674797033677035' // 3gp5
349 },
350 {
351 offset: 4,
352 signature: '667479706d703431' // mp41
353 },
354 {
355 offset: 4,
356 signature: '667479706d703432' // mp42
357 },
358 {
359 offset: 4,
360 signature: '667479704d534e56' // MSNV
361 },
362 {
363 offset: 4,
364 signature: '667479704d345620' // M4V
365 }
366 ]
367};
368
369Object.freeze(this.FILE_SIGNATURES);
370
371/**
372 * Extracts a tar file to the given directory.
373 *
374 * @method extract
375 * @static
376 * @async
377 * @param {String} filePath Path of the file to extract
378 * @param {String} destinationPath Path of the directory where to
379 * extract files
380 * @param {Function} [callback] The function to call when done
381 * - **Error** The error if an error occurred, null otherwise
382 */
383module.exports.extract = function(filePath, destinationPath, callback) {
384 var extractTimeout;
385 var streamError;
386 var extractDone = false;
387
388 callback = callback || function(error) {
389 if (error)
390 process.logger.error('Extract error', {error: error});
391 else
392 process.logger.silly(filePath + ' extracted into ' + destinationPath);
393 };
394
395 if (filePath && destinationPath) {
396
397 // Prepare the extractor with destination path
398 var extractor = tar.extract(path.normalize(destinationPath));
399
400 var onError = function(error) {
401 process.logger.error(error);
402 process.logger.verbose('Extract error', {path: destinationPath});
403 if (extractTimeout)
404 clearTimeout(extractTimeout);
405
406 streamError = error;
407 extractor.end();
408 };
409
410 // Handle extraction end
411 extractor.on('finish', function() {
412 extractDone = true;
413 process.logger.silly('extractor end', {path: destinationPath});
414 if (extractTimeout)
415 clearTimeout(extractTimeout);
416
417 callback(streamError);
418 });
419
420 var tarFileReadableStream = fs.createReadStream(path.normalize(filePath));
421
422 // Handle errors
423 tarFileReadableStream.on('error', onError);
424 extractor.on('error', onError);
425
426 // Listen to readable stream close event
427 tarFileReadableStream.on('close', function(chunk) {
428 process.logger.silly('stream closed', {path: destinationPath});
429
430 // In case of a broken archive, the readable stream close event is dispatched but not the close event of the
431 // writable stream, wait for 10 seconds and dispatch an error if writable stream is still not closed
432 if (!extractDone)
433 extractTimeout = setTimeout(onError, 30000, new Error('Unexpected end of archive'));
434
435 });
436
437 // Extract file
438 tarFileReadableStream.pipe(extractor);
439
440 } else
441 callback(new TypeError('Invalid filePath and / or destinationPath, expected strings'));
442};
443
444/**
445 * Copies a file or a directory.
446 *
447 * @method copy
448 * @static
449 * @async
450 * @param {String} sourcePath Path of the source to copy
451 * @param {String} destinationSourcePath Final path of the source
452 * @param {Function} callback The function to call when done
453 * - **Error** The error if an error occurred, null otherwise
454 */
455module.exports.copy = function(sourcePath, destinationSourcePath, callback) {
456 var self = this;
457
458 // Get source stats to test if this is a directory or a file
459 fs.stat(sourcePath, function(error, stats) {
460 if (error)
461 return callback(error);
462
463 if (stats.isDirectory()) {
464
465 // Resource is a directory
466
467 // Open directory
468 fs.readdir(sourcePath, function(error, resources) {
469
470 // Failed reading directory
471 if (error)
472 return callback(error);
473
474 var pendingResourceNumber = resources.length;
475
476 // Directory is empty, create it and leave
477 if (!pendingResourceNumber) {
478 self.mkdir(destinationSourcePath, callback);
479 return;
480 }
481
482 // Iterate through the list of resources in the directory
483 resources.forEach(function(resource) {
484 var resourcePath = path.join(sourcePath, resource);
485 var resourceDestinationPath = path.join(destinationSourcePath, resource);
486
487 // Copy resource
488 self.copy(resourcePath, resourceDestinationPath, function(error) {
489 if (error)
490 return callback(error);
491
492 pendingResourceNumber--;
493
494 if (!pendingResourceNumber)
495 callback();
496 });
497 });
498
499 });
500
501 } else {
502
503 // Resource is a file
504 copyFile.call(self, sourcePath, destinationSourcePath, callback);
505
506 }
507
508 });
509
510};
511
512/**
513 * Gets a JSON file content.
514 *
515 * This will verify that the file exists first.
516 *
517 * @method getJSONFileContent
518 * @static
519 * @async
520 * @param {String} filePath The path of the file to read
521 * @param {Function} callback The function to call when done
522 * - **Error** The error if an error occurred, null otherwise
523 * - **String** The file content or null if an error occurred
524 * @throws {TypeError} An error if callback is not speficied
525 */
526module.exports.getJSONFileContent = function(filePath, callback) {
527 if (!filePath)
528 return callback(new TypeError('Invalid file path, expected a string'));
529
530 // Check if file exists
531 fs.exists(filePath, function(exists) {
532
533 if (exists) {
534
535 // Read file content
536 fs.readFile(filePath, {
537 encoding: 'utf8'
538 },
539 function(error, data) {
540 if (error) {
541 callback(error);
542 } else {
543 var dataAsJson;
544 try {
545
546 // Try to parse file data as JSON content
547 dataAsJson = JSON.parse(data);
548
549 } catch (e) {
550 callback(new Error(e.message));
551 }
552
553 callback(null, dataAsJson);
554 }
555 });
556 } else
557 callback(new Error('Missing file ' + filePath));
558
559 });
560};
561
562/**
563 * Creates a directory.
564 *
565 * If parent directory does not exist, it will be automatically created.
566 * If directory already exists, it won't do anything.
567 *
568 * @method mkdir
569 * @static
570 * @async
571 * @param {String} directoryPath The directory system path to create
572 * @param {Function} [callback] The function to call when done
573 * - **Error** The error if an error occurred, null otherwise
574 */
575module.exports.mkdir = function(directoryPath, callback) {
576 callback = callback || function(error) {
577 if (error)
578 process.logger.error('mkdir error', {error: error});
579 else
580 process.logger.silly(directoryPath + ' directory created');
581 };
582
583 if (!directoryPath)
584 return callback(new TypeError('Invalid directory path, expected a string'));
585
586 fs.exists(directoryPath, function(exists) {
587 if (exists)
588 callback();
589 else
590 mkdirRecursive(directoryPath, callback);
591 });
592};
593
594/**
595 * Removes a directory and all its content recursively and asynchronously.
596 *
597 * @method rmdir
598 * @static
599 * @async
600 * @deprecated Use rm instead
601 * @param {String} directoryPath Path of the directory to remove
602 * @param {Function} [callback] The function to call when done
603 * - **Error** The error if an error occurred, null otherwise
604 */
605module.exports.rmdir = function(directoryPath, callback) {
606 callback = callback || function(error) {
607 if (error)
608 process.logger.error('rmdir error', {error: error});
609 else
610 process.logger.silly(directoryPath + ' directory removed');
611 };
612
613 if (!directoryPath)
614 return callback(new TypeError('Invalid directory path, expected a string'));
615
616 fs.exists(directoryPath, function(exists) {
617 if (!exists)
618 callback();
619 else
620 rmdirRecursive(directoryPath, callback);
621 });
622};
623
624/**
625 * Gets OpenVeo configuration directory path.
626 *
627 * OpenVeo configuration is stored in user home directory.
628 *
629 * @method getConfDir
630 * @static
631 * @return {String} OpenVeo configuration directory path
632 */
633module.exports.getConfDir = function() {
634 var env = process.env;
635 var home = env.HOME;
636
637 if (process.platform === 'win32')
638 home = env.USERPROFILE || env.HOMEDRIVE + env.HOMEPATH || home || '';
639
640 return path.join(home, '.openveo');
641};
642
643/**
644 * Gets the content of a directory recursively and asynchronously.
645 *
646 * @method readdir
647 * @static
648 * @async
649 * @param {String} directoryPath Path of the directory
650 * @param {Function} callback The function to call when done
651 * - **Error** The error if an error occurred, null otherwise
652 * - **Array** The list of resources insides the directory
653 */
654module.exports.readdir = function(directoryPath, callback) {
655 if (!directoryPath || Object.prototype.toString.call(directoryPath) !== '[object String]')
656 return callback(new TypeError('Invalid directory path, expected a string'));
657
658 fs.stat(directoryPath, function(error, stat) {
659 if (error) callback(error);
660 else if (!stat.isDirectory())
661 callback(new Error(directoryPath + ' is not a directory'));
662 else
663 readdirRecursive(directoryPath, callback);
664 });
665};
666
667/**
668 * Gets part of a file as bytes.
669 *
670 * @method readFile
671 * @static
672 * @async
673 * @param {String} filePath Path of the file
674 * @param {Number} [offset] Specify where to begin reading from in the file
675 * @param {Number} [length] The number of bytes ro read
676 * @param {Function} callback The function to call when done
677 * - **Error** The error if an error occurred, null otherwise
678 * - **Buffer** The buffer containing read bytes
679 */
680module.exports.readFile = function(filePath, offset, length, callback) {
681 fs.stat(filePath, function(error, stats) {
682 if (error) return callback(error);
683
684 fs.open(filePath, 'r', function(error, fd) {
685 if (error) return callback(error);
686
687 length = length || stats.size - offset;
688 length = Math.min(length, stats.size - offset);
689 var buffer = new Buffer(Math.min(stats.size, length));
690
691 fs.read(fd, buffer, 0, length, offset, function(error, bytesRead, buffer) {
692 fs.close(fd, function() {
693 callback(error, buffer);
694 });
695 });
696 });
697 });
698};
699
700/**
701 * Gets file type.
702 *
703 * @method getFileTypeFromBuffer
704 * @static
705 * @param {Buffer} file At least the first 300 bytes of the file
706 * @return {String} The file type
707 */
708module.exports.getFileTypeFromBuffer = function(file) {
709 for (var type in this.FILE_SIGNATURES) {
710 for (var i = 0; i < this.FILE_SIGNATURES[type].length; i++) {
711 var fileMagicNumbers = file.toString(
712 'hex',
713 this.FILE_SIGNATURES[type][i].offset,
714 this.FILE_SIGNATURES[type][i].offset + (this.FILE_SIGNATURES[type][i].signature.length / 2)
715 );
716
717 if (fileMagicNumbers === this.FILE_SIGNATURES[type][i].signature)
718 return type;
719 }
720 }
721
722 return this.FILE_TYPES.UNKNOWN;
723};
724
725/**
726 * Removes a resource.
727 *
728 * If resource is a directory, the whole directory is removed.
729 *
730 * @method rm
731 * @static
732 * @async
733 * @param {String} resourcePath Path of the resource to remove
734 * @param {Function} [callback] The function to call when done
735 * - **Error** The error if an error occurred, null otherwise
736 */
737module.exports.rm = function(resourcePath, callback) {
738 callback = callback || function(error) {
739 if (error)
740 process.logger.error('rm error', {error: error});
741 else
742 process.logger.silly(resourcePath + ' resource removed');
743 };
744
745 if (!resourcePath)
746 return callback(new TypeError('Invalid resource path, expected a string'));
747
748 fs.stat(resourcePath, function(error, stats) {
749 if (error) return callback(error);
750
751 if (stats.isDirectory())
752 rmdirRecursive(resourcePath, callback);
753 else
754 fs.unlink(resourcePath, callback);
755 });
756};