UNPKG

8.26 kBJavaScriptView Raw
1var crypto = require('crypto');
2var fs = require('fs');
3var path = require('path');
4
5var async = require('async');
6
7
8/**
9 * Filter a list of files by mtime.
10 * @param {Array.<string>} paths List of file paths.
11 * @param {Date} time The comparison time.
12 * @param {function(string, Date, function(boolean))} override Override.
13 * @param {function(Err, Array.<string>)} callback Callback called with any
14 * error and a list of files that have mtimes newer than the provided time.
15 */
16var filterPathsByTime = exports.filterPathsByTime = function(paths, time,
17 override, callback) {
18 async.map(paths, fs.stat, function(err, stats) {
19 if (err) {
20 return callback(err);
21 }
22
23 var olderPaths = [];
24 var newerPaths = paths.filter(function(filePath, index) {
25 var newer = stats[index].mtime > time;
26 if (!newer) {
27 olderPaths.push(filePath);
28 }
29 return newer;
30 });
31
32 async.filter(olderPaths, function(filePath, include) {
33 override(filePath, time, include);
34 }, function(overrides) {
35 callback(null, newerPaths.concat(overrides));
36 });
37 });
38};
39
40
41/**
42 * Determine if any of the given files are newer than the provided time.
43 * @param {Array.<string>} paths List of file paths.
44 * @param {Date} time The comparison time.
45 * @param {function(string, Date, function(boolean))} override Override.
46 * @param {function(Err, boolean)} callback Callback called with any error and
47 * a boolean indicating whether any one of the supplied files is newer than
48 * the comparison time.
49 */
50var anyNewer = exports.anyNewer = function(paths, time, override, callback) {
51 if (paths.length === 0) {
52 process.nextTick(function() {
53 callback(null, false);
54 });
55 return;
56 }
57 var complete = 0;
58 function iterate() {
59 fs.stat(paths[complete], function(err, stats) {
60 if (err) {
61 return callback(err);
62 }
63 if (stats.mtime > time) {
64 return callback(null, true);
65 } else {
66 override(paths[complete], time, function(include) {
67 if (include) {
68 callback(null, true);
69 } else {
70 ++complete;
71 if (complete >= paths.length) {
72 return callback(null, false);
73 }
74 iterate();
75 }
76 });
77 }
78 });
79 }
80 iterate();
81};
82
83
84/**
85 * Filter a list of file config objects by time. Source files on the provided
86 * objects are removed if they have not been modified since the provided time
87 * or any dest file mtime for a dest file on the same object.
88 * @param {Array.<Object>} files A list of Grunt file config objects. These
89 * are returned from `grunt.task.normalizeMultiTaskFiles` and have a src
90 * property (Array.<string>) and an optional dest property (string).
91 * @param {Date} previous Comparison time.
92 * @param {function(string, Date, function(boolean))} override Override.
93 * @param {function(Error, Array.<Object>)} callback Callback called with
94 * modified file config objects. Objects with no more src files are
95 * filtered from the results.
96 */
97var filterFilesByTime = exports.filterFilesByTime = function(files, previous,
98 override, callback) {
99 async.map(files, function(obj, done) {
100 if (obj.dest && !(obj.src.length === 1 && obj.dest === obj.src[0])) {
101 fs.stat(obj.dest, function(err, stats) {
102 if (err) {
103 // dest file not yet created, use all src files
104 return done(null, obj);
105 }
106 return anyNewer(obj.src, stats.mtime, override, function(err, any) {
107 done(err, any && obj);
108 });
109 });
110 } else {
111 filterPathsByTime(obj.src, previous, override, function(err, src) {
112 if (err) {
113 return done(err);
114 }
115 done(null, {src: src, dest: obj.dest});
116 });
117 }
118 }, function(err, results) {
119 if (err) {
120 return callback(err);
121 }
122 // get rid of file config objects with no src files
123 callback(null, results.filter(function(obj) {
124 return obj && obj.src && obj.src.length > 0;
125 }));
126 });
127};
128
129
130/**
131 * Get path to cached file hash for a target.
132 * @param {string} cacheDir Path to cache dir.
133 * @param {string} taskName Task name.
134 * @param {string} targetName Target name.
135 * @param {string} filePath Path to file.
136 * @return {string} Path to hash.
137 */
138var getHashPath = exports.getHashPath = function(cacheDir, taskName, targetName,
139 filePath) {
140 var hashedName = crypto.createHash('md5').update(filePath).digest('hex');
141 return path.join(cacheDir, taskName, targetName, 'hashes', hashedName);
142};
143
144
145/**
146 * Get an existing hash for a file (if it exists).
147 * @param {string} filePath Path to file.
148 * @param {string} cacheDir Cache directory.
149 * @param {string} taskName Task name.
150 * @param {string} targetName Target name.
151 * @param {function(Error, string} callback Callback called with an error and
152 * file hash (or null if the file doesn't exist).
153 */
154var getExistingHash = exports.getExistingHash = function(filePath, cacheDir,
155 taskName, targetName, callback) {
156 var hashPath = getHashPath(cacheDir, taskName, targetName, filePath);
157 fs.exists(hashPath, function(exists) {
158 if (!exists) {
159 return callback(null, null);
160 }
161 fs.readFile(hashPath, callback);
162 });
163};
164
165
166/**
167 * Generate a hash (md5sum) of a file contents.
168 * @param {string} filePath Path to file.
169 * @param {function(Error, string)} callback Callback called with any error and
170 * the hash of the file contents.
171 */
172var generateFileHash = exports.generateFileHash = function(filePath, callback) {
173 var md5sum = crypto.createHash('md5');
174 var stream = new fs.ReadStream(filePath);
175 stream.on('data', function(chunk) {
176 md5sum.update(chunk);
177 });
178 stream.on('error', callback);
179 stream.on('end', function() {
180 callback(null, md5sum.digest('hex'));
181 });
182};
183
184
185/**
186 * Filter files based on hashed contents.
187 * @param {Array.<string>} paths List of paths to files.
188 * @param {string} cacheDir Cache directory.
189 * @param {string} taskName Task name.
190 * @param {string} targetName Target name.
191 * @param {function(Error, Array.<string>)} callback Callback called with any
192 * error and a filtered list of files that only includes files with hashes
193 * that are different than the cached hashes for the same files.
194 */
195var filterPathsByHash = exports.filterPathsByHash = function(paths, cacheDir,
196 taskName, targetName, callback) {
197 async.filter(paths, function(filePath, done) {
198 async.parallel({
199 previous: function(cb) {
200 getExistingHash(filePath, cacheDir, taskName, targetName, cb);
201 },
202 current: function(cb) {
203 generateFileHash(filePath, cb);
204 }
205 }, function(err, hashes) {
206 if (err) {
207 return callback(err);
208 }
209 done(String(hashes.previous) !== String(hashes.current));
210 });
211 }, callback);
212};
213
214
215/**
216 * Filter a list of file config objects based on comparing hashes of src files.
217 * @param {Array.<Object>} files List of file config objects.
218 * @param {string} taskName Task name.
219 * @param {string} targetName Target name.
220 * @param {function(Error, Array.<Object>)} callback Callback called with a
221 * filtered list of file config objects. Object returned will only include
222 * src files with hashes that are different than any cached hashes. Config
223 * objects with no src files will be filtered from the list.
224 */
225var filterFilesByHash = exports.filterFilesByHash = function(files, taskName,
226 targetName, callback) {
227 var modified = false;
228 async.map(files, function(obj, done) {
229
230 filterPathsByHash(obj.src, taskName, targetName, function(err, src) {
231 if (err) {
232 return done(err);
233 }
234 if (src.length) {
235 modified = true;
236 }
237 done(null, {src: src, dest: obj.dest});
238 });
239
240 }, function(err, newerFiles) {
241 callback(err, newerFiles, modified);
242 });
243};
244
245
246/**
247 * Get the path to the cached timestamp for a target.
248 * @param {string} cacheDir Path to cache dir.
249 * @param {string} taskName Task name.
250 * @param {string} targetName Target name.
251 * @return {string} Path to timestamp.
252 */
253var getStampPath = exports.getStampPath = function(cacheDir, taskName,
254 targetName) {
255 return path.join(cacheDir, taskName, targetName, 'timestamp');
256};