273 lines
8.9 KiB
JavaScript
273 lines
8.9 KiB
JavaScript
var crypto = require('crypto');
|
|
var fs = require('fs');
|
|
var path = require('path');
|
|
|
|
var async = require('async');
|
|
|
|
/**
|
|
* Filter a list of files by mtime.
|
|
* @param {Array.<string>} paths List of file paths.
|
|
* @param {Date} time The comparison time.
|
|
* @param {number} tolerance Maximum time in milliseconds that the destination
|
|
* file is allowed to be newer than the source file to compensate for
|
|
* imprecisions in modification times in file systems.
|
|
* @param {function(string, Date, function(boolean))} override Override.
|
|
* @param {function(Err, Array.<string>)} callback Callback called with any
|
|
* error and a list of files that have mtimes newer than the provided time.
|
|
*/
|
|
var filterPathsByTime = exports.filterPathsByTime = function(paths, time,
|
|
tolerance, override, callback) {
|
|
async.map(paths, fs.stat, function(err, stats) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
var olderPaths = [];
|
|
var newerPaths = paths.filter(function(filePath, index) {
|
|
var newer = stats[index].mtime - time > tolerance;
|
|
if (!newer) {
|
|
olderPaths.push(filePath);
|
|
}
|
|
return newer;
|
|
});
|
|
|
|
async.filter(olderPaths, function(filePath, include) {
|
|
override(filePath, time, include);
|
|
}, function(overrides) {
|
|
callback(null, newerPaths.concat(overrides));
|
|
});
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Determine if any of the given files are newer than the provided time.
|
|
* @param {Array.<string>} paths List of file paths.
|
|
* @param {Date} time The comparison time.
|
|
* @param {number} tolerance Maximum time in milliseconds that the destination
|
|
* file is allowed to be newer than the source file to compensate for
|
|
* imprecisions in modification times in file systems.
|
|
* @param {function(string, Date, function(boolean))} override Override.
|
|
* @param {function(Err, boolean)} callback Callback called with any error and
|
|
* a boolean indicating whether any one of the supplied files is newer than
|
|
* the comparison time.
|
|
*/
|
|
var anyNewer = exports.anyNewer = function(paths, time, tolerance, override,
|
|
callback) {
|
|
if (paths.length === 0) {
|
|
process.nextTick(function() {
|
|
callback(null, false);
|
|
});
|
|
return;
|
|
}
|
|
var complete = 0;
|
|
function iterate() {
|
|
fs.stat(paths[complete], function(err, stats) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
var pathTime = stats.mtime.getTime();
|
|
var comparisonTime = time.getTime();
|
|
var difference = pathTime - comparisonTime;
|
|
|
|
if (difference > tolerance) {
|
|
return callback(null, true);
|
|
} else {
|
|
override(paths[complete], time, function(include) {
|
|
if (include) {
|
|
callback(null, true);
|
|
} else {
|
|
++complete;
|
|
if (complete >= paths.length) {
|
|
return callback(null, false);
|
|
}
|
|
iterate();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
iterate();
|
|
};
|
|
|
|
|
|
/**
|
|
* Filter a list of file config objects by time. Source files on the provided
|
|
* objects are removed if they have not been modified since the provided time
|
|
* or any dest file mtime for a dest file on the same object.
|
|
* @param {Array.<Object>} files A list of Grunt file config objects. These
|
|
* are returned from `grunt.task.normalizeMultiTaskFiles` and have a src
|
|
* property (Array.<string>) and an optional dest property (string).
|
|
* @param {Date} previous Comparison time.
|
|
* @param {number} tolerance Maximum time in milliseconds that the destination
|
|
* file is allowed to be newer than the source file to compensate for
|
|
* imprecisions in modification times in file systems.
|
|
* @param {function(string, Date, function(boolean))} override Override.
|
|
* @param {function(Error, Array.<Object>)} callback Callback called with
|
|
* modified file config objects. Objects with no more src files are
|
|
* filtered from the results.
|
|
*/
|
|
var filterFilesByTime = exports.filterFilesByTime = function(files, previous,
|
|
tolerance, override, callback) {
|
|
async.map(files, function(obj, done) {
|
|
if (obj.dest && !(obj.src.length === 1 && obj.dest === obj.src[0])) {
|
|
fs.stat(obj.dest, function(err, stats) {
|
|
if (err) {
|
|
// dest file not yet created, use all src files
|
|
return done(null, obj);
|
|
}
|
|
return anyNewer(
|
|
obj.src, stats.mtime, tolerance, override, function(err, any) {
|
|
done(err, any && obj);
|
|
});
|
|
});
|
|
} else {
|
|
filterPathsByTime(
|
|
obj.src, previous, tolerance, override, function(err, src) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
done(null, {src: src, dest: obj.dest});
|
|
});
|
|
}
|
|
}, function(err, results) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
// get rid of file config objects with no src files
|
|
callback(null, results.filter(function(obj) {
|
|
return obj && obj.src && obj.src.length > 0;
|
|
}));
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Get path to cached file hash for a target.
|
|
* @param {string} cacheDir Path to cache dir.
|
|
* @param {string} taskName Task name.
|
|
* @param {string} targetName Target name.
|
|
* @param {string} filePath Path to file.
|
|
* @return {string} Path to hash.
|
|
*/
|
|
var getHashPath = exports.getHashPath = function(cacheDir, taskName, targetName,
|
|
filePath) {
|
|
var hashedName = crypto.createHash('md5').update(filePath).digest('hex');
|
|
return path.join(cacheDir, taskName, targetName, 'hashes', hashedName);
|
|
};
|
|
|
|
|
|
/**
|
|
* Get an existing hash for a file (if it exists).
|
|
* @param {string} filePath Path to file.
|
|
* @param {string} cacheDir Cache directory.
|
|
* @param {string} taskName Task name.
|
|
* @param {string} targetName Target name.
|
|
* @param {function(Error, string} callback Callback called with an error and
|
|
* file hash (or null if the file doesn't exist).
|
|
*/
|
|
var getExistingHash = exports.getExistingHash = function(filePath, cacheDir,
|
|
taskName, targetName, callback) {
|
|
var hashPath = getHashPath(cacheDir, taskName, targetName, filePath);
|
|
fs.exists(hashPath, function(exists) {
|
|
if (!exists) {
|
|
return callback(null, null);
|
|
}
|
|
fs.readFile(hashPath, callback);
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Generate a hash (md5sum) of a file contents.
|
|
* @param {string} filePath Path to file.
|
|
* @param {function(Error, string)} callback Callback called with any error and
|
|
* the hash of the file contents.
|
|
*/
|
|
var generateFileHash = exports.generateFileHash = function(filePath, callback) {
|
|
var md5sum = crypto.createHash('md5');
|
|
var stream = new fs.ReadStream(filePath);
|
|
stream.on('data', function(chunk) {
|
|
md5sum.update(chunk);
|
|
});
|
|
stream.on('error', callback);
|
|
stream.on('end', function() {
|
|
callback(null, md5sum.digest('hex'));
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Filter files based on hashed contents.
|
|
* @param {Array.<string>} paths List of paths to files.
|
|
* @param {string} cacheDir Cache directory.
|
|
* @param {string} taskName Task name.
|
|
* @param {string} targetName Target name.
|
|
* @param {function(Error, Array.<string>)} callback Callback called with any
|
|
* error and a filtered list of files that only includes files with hashes
|
|
* that are different than the cached hashes for the same files.
|
|
*/
|
|
var filterPathsByHash = exports.filterPathsByHash = function(paths, cacheDir,
|
|
taskName, targetName, callback) {
|
|
async.filter(paths, function(filePath, done) {
|
|
async.parallel({
|
|
previous: function(cb) {
|
|
getExistingHash(filePath, cacheDir, taskName, targetName, cb);
|
|
},
|
|
current: function(cb) {
|
|
generateFileHash(filePath, cb);
|
|
}
|
|
}, function(err, hashes) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
done(String(hashes.previous) !== String(hashes.current));
|
|
});
|
|
}, callback);
|
|
};
|
|
|
|
|
|
/**
|
|
* Filter a list of file config objects based on comparing hashes of src files.
|
|
* @param {Array.<Object>} files List of file config objects.
|
|
* @param {string} taskName Task name.
|
|
* @param {string} targetName Target name.
|
|
* @param {function(Error, Array.<Object>)} callback Callback called with a
|
|
* filtered list of file config objects. Object returned will only include
|
|
* src files with hashes that are different than any cached hashes. Config
|
|
* objects with no src files will be filtered from the list.
|
|
*/
|
|
var filterFilesByHash = exports.filterFilesByHash = function(files, taskName,
|
|
targetName, callback) {
|
|
var modified = false;
|
|
async.map(files, function(obj, done) {
|
|
|
|
filterPathsByHash(obj.src, taskName, targetName, function(err, src) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
if (src.length) {
|
|
modified = true;
|
|
}
|
|
done(null, {src: src, dest: obj.dest});
|
|
});
|
|
|
|
}, function(err, newerFiles) {
|
|
callback(err, newerFiles, modified);
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Get the path to the cached timestamp for a target.
|
|
* @param {string} cacheDir Path to cache dir.
|
|
* @param {string} taskName Task name.
|
|
* @param {string} targetName Target name.
|
|
* @return {string} Path to timestamp.
|
|
*/
|
|
var getStampPath = exports.getStampPath = function(cacheDir, taskName,
|
|
targetName) {
|
|
return path.join(cacheDir, taskName, targetName, 'timestamp');
|
|
};
|