Source: app-wrapper/js/lib/fileManager.js
/**
* @fileOverview FileManager class file
* @author Dino Ivankov <dinoivankov@gmail.com>
* @version 1.3.1
*/
const _ = require('lodash');
const path = require('path');
const fs = require('fs');
const archiver = require('archiver');
const AppBaseClass = require('./appBase').AppBaseClass;
/**
* A class for file operations
*
* @class
* @extends {appWrapper.AppBaseClass}
* @memberOf appWrapper
*
* @property {array} watchedFiles An array with absolute watched file paths
* @property {Object} watched Object that stores references to unwatch methods for watched files
*/
class FileManager extends AppBaseClass {
/**
* Creates FileManager instance
*
* @constructor
* @return {FileManager} Instance of FileManager class
*/
constructor(){
super();
this.watchedFiles = [];
this.watched = {};
}
/**
* Shuts down file manager, unwatching all files and removing leftover references
*
* @async
* @return {Boolean} Shutdown result
*/
async shutdown () {
return await this.unwatchAllFiles();
}
/**
* Checks whether given file exists
*
* @param {string} file Absolute file path
* @return {Boolean} True if file exists, false otherwise
*/
fileExists(file){
var fileExists = true;
var filePath = path.resolve(file);
if (fs.existsSync(filePath)){
fileExists = true;
} else {
fileExists = false;
}
return fileExists;
}
/**
* Checks whether given path is a file
*
* @async
* @param {string} file Absolute file path
* @return {Boolean} True if file is file, false otherwise
*/
async isFile(file){
if (!file){
return false;
}
var filePath = path.resolve(file);
var isFile = true;
var exists = this.fileExists(filePath);
if (exists){
var fileStat = fs.statSync(filePath);
if (!fileStat.isFile()){
isFile = false;
}
} else {
isFile = false;
}
return isFile;
}
/**
* Checks whether given path is a directory
*
* @param {string} dir Absolute directory path
* @return {Boolean} True if file is a directory, false otherwise
*/
isDir(dir){
var dirPath = path.resolve(dir);
var isDir = true;
var exists = this.fileExists(dirPath);
if (exists){
var fileStat = fs.statSync(dirPath);
if (!fileStat.isDirectory()){
isDir = false;
}
} else {
isDir = false;
}
return isDir;
}
/**
* Checks whether given dir is writable by current user
*
* @param {string} dir Absolute directory path
* @return {Boolean} True if directory is writable, false otherwise
*/
isDirWritable(dir){
var dirValid = true;
var dirPath = path.resolve(dir);
if (fs.existsSync(dirPath)){
var dirStat = fs.statSync(dirPath);
if (!dirStat.isDirectory()){
dirValid = false;
} else {
var fileName = (Math.random() + '').replace(/[^\d]+/g, '') + '.tmp';
var filePath = path.join(dirPath, fileName);
var fileHandle = false;
try {
fileHandle = fs.openSync(filePath, 'a', 0x775);
} catch (e) {
this.log('Can\'t open file "{1}" for testing permissions - {2} ', 'error', [filePath, e]);
}
if (!fileHandle){
dirValid = false;
} else {
fs.closeSync(fileHandle);
try {
fs.unlinkSync(filePath);
} catch (e){
this.log('Can\'t delete temporary file "{1}" - {2} ', 'error', [filePath, e]);
}
}
}
} else {
dirValid = false;
}
return dirValid;
}
/**
* Checks whether given file is writable by current user
*
* @param {string} file Absolute file path
* @return {Boolean} True if file is writable, false otherwise
*/
isFileWritable (file){
var fileValid = true;
if (!file){
fileValid = false;
} else {
var filePath = path.resolve(file);
var dirPath = path.dirname(filePath);
if (fs.existsSync(filePath)){
let fileHandle = false;
try {
fileHandle = fs.openSync(filePath, 'a', 0x775);
} catch (e) {
fileValid = false;
this.log('Can\'t open file "{1}" for testing permissions - {2} ', 'error', [filePath, e]);
}
if (!fileHandle){
fileValid = false;
} else {
fs.closeSync(fileHandle);
}
} else {
if (fs.existsSync(dirPath)){
var dirStat = fs.statSync(dirPath);
if (!dirStat.isDirectory()){
fileValid = false;
} else {
let fileHandle = false;
try {
fileHandle = fs.openSync(filePath, 'a', 0x775);
} catch (e) {
fileValid = false;
this.log('Can\'t open file "{1}" in dir {2} for testing permissions - {3} ', 'error', [path.basename(filePath), dirPath, e]);
}
if (!fileHandle){
fileValid = false;
} else {
fs.closeSync(fileHandle);
try {
fs.unlinkSync(filePath);
} catch (e){
this.log('Can\'t delete temporary file "{1}" - {2} ', 'error', [filePath, e]);
}
}
}
} else {
fileValid = false;
}
}
}
return fileValid;
}
/**
* Returns absolute path to app root directory
*
* @return {string} App directory absolute path
*/
getAppRoot () {
var appRoot = path.dirname(process.execPath);
if (!fs.existsSync(path.resolve(appRoot + '/package.json'))){
appRoot = path.resolve('.');
}
return appRoot;
}
/**
* Compresses files into zip archive
*
* @async
* @param {string} archivePath Absolute path to zip archive
* @param {string[]} files An array of file paths to compress
* @return {(string|boolean)} Zip archive path or false if compression failed
*/
async zipFiles(archivePath, files){
var resolveReference;
var returnPromise = new Promise((resolve) => {
resolveReference = resolve;
});
var output = fs.createWriteStream(archivePath);
var archive = archiver('zip', {
zlib: { level: 9 } // Sets the compression level.
});
output.on('close', function() {
resolveReference(archivePath);
});
archive.on('error', function(err) {
resolveReference(false);
this.log('Error zipping data: "{1}"', 'error', [err]);
});
archive.pipe(output);
_.each(files, function(file){
var filePath = file;
var fileName = path.basename(file);
archive.append(fs.createReadStream(filePath), { name: fileName });
});
archive.finalize();
return returnPromise;
}
/**
* Creates directory recursively
*
* @async
* @param {string} directory Absolute directory path
* @param {Number} mode Octal mode definition (i.e. 0o775)
* @return {Boolean} Result of directory creation
*/
async createDirRecursive(directory, mode){
var dirName = path.resolve(directory);
var dirChunks = dirName.split(path.sep);
var dirPath = '';
if (fs.existsSync(dirName)){
if (await this.isFile(dirName)){
this.log('Can\'t create directory "{1}", already exists and it is a file.', 'error', [dirName]);
return false;
}
} else {
dirPath = dirChunks[0];
for(let i=1; i< dirChunks.length;i++){
dirPath = path.join(dirPath, path.sep + dirChunks[i]);
if (!fs.existsSync(dirPath)){
fs.mkdirSync(dirPath, mode);
} else if (await this.isFile(dirPath)){
this.log('Can\'t create directory "{1}", already exists and it is a file.', 'error', [dirPath]);
return false;
}
}
}
return fs.existsSync(dirName);
}
/**
* Creates directory (recursive) and writes file to it
*
* @async
* @param {string} fileName Absolute path to file
* @param {Number} mode Octal mode definition (i.e. 0o775)
* @param {Object} options Options object for fs.writeFileSync
* @param {string} data Data to write to file
* @return {Boolean} True if operation succeeded, false otherwise
*/
async createDirFileRecursive(fileName, mode, options, data){
if (!options){
options = {flag: 'w'};
}
if (!data){
data = '';
}
if (!mode){
mode = 0o755;
}
var filePath = path.resolve(fileName);
var dirName = path.dirname(filePath);
var dirCreated = await this.createDirRecursive(dirName, mode);
if (!dirCreated){
return false;
} else {
try {
fs.writeFileSync(filePath, data, options);
return await this.isFile(filePath);
} catch (ex) {
this.log('Can\'t create file "{1}" - "{2}".', 'error', [filePath, ex && ex.message ? ex.message : ex]);
return false;
}
}
}
/**
* Writes file to disk
*
* @async
* @param {string} file Absolute path to file
* @param {string} data Data to write
* @param {Object} options Options object for fs.writeFileSync
* @return {Boolean} True if operation succeeded, false otherwise
*/
async writeFileSync(file, data, options){
var saved = false;
try {
saved = fs.writeFileSync(file, data, options);
} catch (ex) {
console.log(ex);
}
return saved;
}
/**
* Reads file from disk
*
* @param {string} file Absolute path to file
* @param {Object} options Options object for fs.writeFileSync
* @return {(string|null)} File contents if operation succeeded, null otherwise
*/
readFileSync(file, options){
var data = null;
try {
data = fs.readFileSync(file, options) + '';
} catch (ex) {
console.log(ex);
}
return data;
}
/**
* Add listener that gets called when file changes on disk
*
* @async
* @param {string} filePath Absolute path to file
* @param {Object} options Options object for fs.watchFile
* @param {Function} listener Method to call when file changes
* @return {undefined}
*/
async watchFile(filePath, options, listener){
if (await this.isFile(filePath)){
this.watchedFiles.push(filePath);
fs.watchFile(filePath, options, listener);
}
}
/**
* Removes listener that is bound to be called when file changes on disk
*
* @async
* @param {string} filePath Absolute path to file
* @param {Function} listener Method to call when file changes
* @return {undefined}
*/
async unWatchFile(filePath, listener){
var watchIndex = _.indexOf(this.watchedFiles, filePath);
if (watchIndex != -1){
fs.unwatchFile(filePath, listener);
_.pullAt(this.watchedFiles, watchIndex);
}
}
/**
* Removes listeners for all watched files
*
* @async
* @return {undefined}
*/
async unwatchAllFiles () {
let watchedFiles = _.clone(this.watchedFiles);
for (let i=0; i<watchedFiles.length; i++){
await this.unwatchFile(watchedFiles[i]);
}
this.watchedFiles = [];
}
/**
* Add listener that gets called when file changes on disk
*
* @async
* @param {string} filePath Absolute path to file
* @param {Object} options Options object for fs.watchFile
* @param {Function} listener Method to call when file changes
* @return {undefined}
*/
async watch(filePath, options, listener){
if (await this.isFile(filePath)){
var listenerName = listener.name ? listener.name : listener;
this.watched[filePath + ':' + listenerName] = fs.watch(filePath, options, listener);
}
}
/**
* Removes listener that is bound to be called when file changes on disk
*
* @async
* @param {string} filePath Absolute path to file
* @param {Function} listener Method to call when file changes
* @return {undefined}
*/
async unwatch(filePath, listener){
var listenerName = listener.name ? listener.name : listener;
if (this.watched && this.watched[filePath + ':' + listenerName] && this.watched[filePath + ':' + listenerName].close && _.isFunction(this.watched[filePath + ':' + listenerName].close)){
this.watched[filePath + ':' + listenerName].close();
delete this.watched[filePath + ':' + listenerName];
}
}
/**
* Removes listeners for all watched files
*
* @async
* @return {undefined}
*/
async unwatchAll () {
for (let name in this.watched){
this.watched[name].close();
delete this.watched[name];
}
this.watched = [];
}
/**
* Loads all files from given directory that match extension regex from argument
*
* @async
* @param {string} directory Absolute path to directory
* @param {string} extensionMatch Regex string for extension matching
* @param {Boolean} requireFiles Flag to indicate whether to require() files or return their contents as strings
* @param {Boolean} notSilent Flag to indicate whether to log outout
* @return {Object} File contents (or required object) by file name
*/
async loadFilesFromDir (directory, extensionMatch, requireFiles, notSilent) {
var filesData = {};
var extRegex;
if (!extensionMatch){
extRegex = /.*/;
} else if (_.isString(extensionMatch)){
extensionMatch = extensionMatch.replace(/^\//, '').replace(/\/$/, '').replace(/[^\\]$/, '').replace(/[^\\]\./, '');
extRegex = new RegExp(extensionMatch);
} else {
extRegex = extensionMatch;
extensionMatch = (extensionMatch + '').replace(/^\//, '').replace(/\/$/, '');
}
// directory = path.resolve(directory);
if (fs.existsSync(directory)){
var stats = fs.statSync(directory);
if (stats.isDirectory()){
// this.log('Loading files from "{1}"...', 'debug', [directory]);
var files = fs.readdirSync(directory);
var eligibleFiles = _.filter(files, (file) => {
var fileStats = fs.statSync(path.join(directory, file));
if (fileStats.isFile()){
return true;
} else {
// this.log('Omitting file "{1}" - file is a directory.', 'debug', [file]);
}
});
eligibleFiles = _.filter(eligibleFiles, (file) => {
if (file.match(extRegex)){
return true;
} else {
// this.log('Omitting file "{1}", extension invalid.', 'debug', [file]);
}
});
eligibleFiles = _.map(eligibleFiles, (file) => {
return path.join(directory, file);
});
var filesToLoad = _.filter(eligibleFiles, (file) => {
var fileStat = fs.statSync(file);
if (fileStat.isFile()){
return true;
} else {
// this.log('Omitting file "{1}", not a file.', 'warning', [path.basename(file)]);
}
});
if (filesToLoad && filesToLoad.length){
// this.log('Found {1} eligible files of {2} total files in "{3}"...', 'debug', [filesToLoad.length, files.length, directory]);
for (var i =0 ; i < filesToLoad.length; i++){
var fullPath = filesToLoad[i];
var fileName = path.basename(fullPath);
var fileIdentifier = fileName;
if (extensionMatch){
fileIdentifier = fileIdentifier.replace(new RegExp(extensionMatch), '');
}
filesData[fileIdentifier] = await this.loadFile(fullPath, requireFiles, notSilent);
}
} else {
// this.log('No eligible files found in "{1}"...', 'debug', [directory]);
}
} else {
// this.log('Directory "{1}" is not a directory!', 'error', [directory]);
filesData = false;
}
} else {
// this.log('Directory "{1}" does not exist!', 'error', [directory]);
filesData = false;
}
return filesData;
}
/**
* Loads file from argument
*
* @async
* @param {string} filePath Absolute path to file
* @param {Boolean} requireFile Flag to indicate whether to require() file or return its contents as string
* @param {Boolean} notSilent Flag to force logging output
* @return {(string|Object)} File contents, exported object or null on failure.
*/
async loadFile (filePath, requireFile, notSilent){
var fileData = null;
var fileName = path.basename(filePath);
var directory = path.dirname(filePath);
if (notSilent) {
this.log('Loading file "{1}" from "{2}"...', 'group', [fileName, directory]);
}
if (!requireFile){
if (fs.existsSync(filePath)){
let fStats = fs.statSync(filePath);
if (fStats.isFile()){
fileData = fs.readFileSync(filePath, {encoding: 'utf8', flag: 'rs+'}).toString();
} else {
if (notSilent) {
this.log('Problem loading file (not a file) "{1}" from "{2}".', 'error', [fileName, directory]);
}
throw new Error('Problem loading file (not a file).');
}
} else {
if (notSilent) {
this.log('Problem loading file (does not exist) "{1}" from "{2}".', 'error', [fileName, directory]);
}
throw new Error('Problem loading file (does not exist).');
}
} else {
try {
delete require.cache[require.resolve(path.resolve(filePath))];
fileData = require(path.resolve(filePath));
if (fileData.exported){
fileData = require(filePath).exported;
} else {
var fileKeys = _.keys(fileData);
if (fileKeys && fileKeys.length && fileKeys[0] && fileData[fileKeys[0]]){
if (notSilent) {
this.log('While requiring file "{1}" from "{2}", "exported" key was not found, using "{3}" key instead.', 'warning', [fileName, directory, fileKeys[0]]);
}
fileData = fileData[fileKeys[0]];
} else {
fileData = null;
if (notSilent) {
this.log('Problem Loading file "{1}" from "{2}", in order to require file, it has to export value "exported"!', 'error', [fileName, directory]);
}
}
}
} catch (ex) {
if (notSilent) {
this.log('Problem requiring file "{1}" - "{2}"', 'error', [filePath, ex.message]);
}
throw ex;
}
}
if (fileData){
if (notSilent) {
this.log('Successfully loaded file "{1}" from "{2}"...', 'info', [fileName, directory]);
}
} else {
if (notSilent) {
this.log('Failed loading file "{1}" from "{2}"...', 'error', [fileName, directory]);
}
throw new Error('Failed loading file ' + filePath);
}
if (notSilent){
this.log('Loading file "{1}" from "{2}"...', 'groupend', [fileName, directory]);
}
return fileData;
}
/**
* Scans given dirs for file and returns first file found
*
* @async
* @param {string} fileName File name (basename)
* @param {string[]} dirs An array of absolute directory paths to search
* @param {Boolean} requireFile Flag to indicate whether to require() file or return its contents as string
* @param {Boolean} notSilent Flag to force logging output
* @return {(string|Object|boolean)} File contents, exported object or false on failure.
*/
async loadFileFromDirs (fileName, dirs, requireFile, notSilent){
let currentFile = await this.getFirstFileFromDirs(fileName, dirs);
if (await this.isFile(currentFile)){
let fileData = await this.loadFile(currentFile, requireFile, notSilent);
if (fileData){
return fileData;
}
}
return false;
}
/**
* Reads file list from dir and returns it, excluding '.' and '..'
*
* @async
* @param {string} path Absolute directory path
* @return {string[]} An array of entries from directory
*/
async readDir(path){
let files = [];
if (await this.isDir(path)){
files = fs.readdirSync(path);
}
return files;
}
/**
* Reads recursive file list from dir and returns it, excluding all '.' and '..' entries
*
* @async
* @param {string} dirPath Absolute directory path
* @param {string} extensionRegex Regex for extension matching
* @return {string[]} An array of entries from directory and its subdirectories
*/
async readDirRecursive(dirPath, extensionRegex){
let files = [];
let rootFiles = await this.readDir(dirPath);
for (let i=0; i < rootFiles.length; i++){
let filePath = path.join(dirPath, rootFiles[i]);
if (await this.isDir(filePath)){
files = _.union(files, await this.readDirRecursive(filePath, extensionRegex));
} else {
if (extensionRegex) {
if (filePath.match(extensionRegex)){
files.push(filePath);
}
} else {
files.push(filePath);
}
}
}
return files;
}
/**
* Scans given directories for file, returning path to first one found
*
* @async
* @param {string} fileName File name (basename)
* @param {string[]} dirs An array of absolute directory paths
* @return {(string|null)} Path to found file or null if no file was found
*/
async getFirstFileFromDirs(fileName, dirs){
for(let i=0; i<dirs.length; i++){
let currentFile = path.join(dirs[i], fileName);
if (await this.isFile(currentFile)){
return currentFile;
}
}
return null;
}
/**
* Deletes file from the file system
*
* @async
* @param {string} filePath Absolute path to file
* @return {Boolean} Operation result
*/
async deleteFile(filePath) {
let deleted = false;
if (await this.isFile(filePath)){
fs.unlinkSync(filePath);
}
deleted = !await this.isFile(filePath);
return deleted;
}
/**
* Copies single file from source to destination
*
* @async
* @param {string} sourceFile Source file path
* @param {string} destinationFile Destination file path
* @return {Boolean} Operation result
*/
async copyFile(sourceFile, destinationFile){
let canCopy = true;
sourceFile = path.resolve(sourceFile);
destinationFile = path.resolve(destinationFile);
if (sourceFile !== destinationFile && this.isFile(sourceFile)){
if (!this.fileExists(destinationFile)){
let destinationFileDir = path.dirname(destinationFile);
if (!this.isDir(destinationFileDir)){
await this.createDirRecursive(destinationFileDir);
}
canCopy = true;
} else {
if (this.isFile(destinationFile) && this.isFileWritable(destinationFile)){
canCopy = true;
}
}
if (canCopy){
let sourceStream = fs.createReadStream(sourceFile);
let destinationStream = fs.createWriteStream(destinationFile);
return new Promise((resolve, reject) => {
sourceStream.on('error', copyFailed);
destinationStream.on('error', copyFailed);
function copyFailed(err) {
sourceStream.destroy();
destinationStream.end();
reject(err);
}
destinationStream.on('finish', () => {
resolve(true);
});
sourceStream.pipe(destinationStream);
});
} else {
return false;
}
} else {
return false;
}
}
/**
* Copies source directory (with subdirs and files) to destination directory
*
* @async
* @param {string} sourceDir Source directory
* @param {string} destinationDir Destination directory
* @return {Boolean} Operation result
*/
async copyDirRecursive(sourceDir, destinationDir){
let copied = false;
let canCopy = true;
sourceDir = path.resolve(sourceDir);
destinationDir = path.resolve(destinationDir);
if (sourceDir == destinationDir){
canCopy = false;
}
if (!await this.fileExists(destinationDir)){
await this.createDirRecursive(destinationDir);
} else {
if (!this.isDir(destinationDir)){
canCopy = false;
}
}
let totalFiles = 0;
let copiedFiles = 0;
if (canCopy && this.isDir(sourceDir)){
let relativePath = path.relative(sourceDir, destinationDir);
let fileList = await this.readDirRecursive(sourceDir);
totalFiles = fileList.length;
for (let i=0; i<totalFiles;i++){
let sourceFile = fileList[i];
let destinationFile = path.resolve(path.join(path.dirname(sourceFile), relativePath, path.basename(sourceFile)));
if (await this.copyFile(sourceFile, destinationFile)){
copiedFiles++;
}
}
if (totalFiles == 0 || copiedFiles > 0){
copied = true;
}
}
return copied;
}
}
exports.FileManager = FileManager;