UNPKG

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