Bug:Fix file validation issue
[vnfsdk/refrepo.git] / vnfmarket / src / main / webapp / vnfmarket / node_modules / karma / lib / file_list.js
1 var fs = require('fs')
2 var glob = require('glob')
3 var mm = require('minimatch')
4 var q = require('q')
5
6 var helper = require('./helper')
7 var log = require('./logger').create('watcher')
8
9 var createWinGlob = function (realGlob) {
10   return function (pattern, options, done) {
11     realGlob(pattern, options, function (err, results) {
12       done(err, results.map(helper.normalizeWinPath))
13     })
14   }
15 }
16
17 var findBucketByPath = function (buckets, path) {
18   for (var i = 0; i < buckets.length; i++) {
19     for (var j = 0; j < buckets[i].length; j++) {
20       if (buckets[i][j].originalPath === path) {
21         return [i, j]
22       }
23     }
24   }
25 }
26
27 if (process.platform === 'win32') {
28   glob = createWinGlob(glob)
29 }
30
31 var File = function (path, mtime) {
32   // used for serving (processed path, eg some/file.coffee -> some/file.coffee.js)
33   this.path = path
34
35   // original absolute path, id of the file
36   this.originalPath = path
37
38   // where the content is stored (processed)
39   this.contentPath = path
40
41   this.mtime = mtime
42   this.isUrl = false
43 }
44
45 var Url = function (path) {
46   this.path = path
47   this.isUrl = true
48 }
49
50 Url.prototype.toString = File.prototype.toString = function () {
51   return this.path
52 }
53
54 var GLOB_OPTS = {
55   // globDebug: true,
56   follow: true,
57   cwd: '/'
58 }
59
60 var byPath = function (a, b) {
61   if (a.path > b.path) {
62     return 1
63   }
64   if (a.path < b.path) {
65     return -1
66   }
67   return 0
68 }
69
70 // TODO(vojta): ignore changes (add/change/remove) when in the middle of refresh
71 // TODO(vojta): do not glob patterns that are watched (both on init and refresh)
72 var List = function (patterns, excludes, emitter, preprocess, batchInterval) {
73   var self = this
74   var pendingDeferred
75   var pendingTimeout
76
77   var errors = []
78
79   var addError = function (path) {
80     if (errors.indexOf(path) === -1) {
81       errors.push(path)
82     }
83   }
84
85   var removeError = function (path) {
86     var idx = errors.indexOf(path)
87
88     if (idx !== -1) {
89       errors.splice(idx, 1)
90     }
91   }
92
93   var resolveFiles = function (buckets) {
94     var uniqueMap = {}
95     var files = {
96       served: [],
97       included: []
98     }
99
100     buckets.forEach(function (bucket, idx) {
101       bucket.sort(byPath).forEach(function (file) {
102         if (!uniqueMap[file.path]) {
103           if (patterns[idx].served) {
104             files.served.push(file)
105           }
106
107           if (patterns[idx].included) {
108             files.included.push(file)
109           }
110
111           uniqueMap[file.path] = true
112         }
113       })
114     })
115
116     return files
117   }
118
119   var resolveDeferred = function (files) {
120     clearPendingTimeout()
121
122     if (!errors.length) {
123       pendingDeferred.resolve(files || resolveFiles(self.buckets))
124     } else {
125       pendingDeferred.reject(errors.slice())
126     }
127
128     pendingDeferred = pendingTimeout = null
129   }
130
131   var fireEventAndDefer = function () {
132     clearPendingTimeout()
133
134     if (!pendingDeferred) {
135       pendingDeferred = q.defer()
136       emitter.emit('file_list_modified', pendingDeferred.promise)
137     }
138
139     pendingTimeout = setTimeout(resolveDeferred, batchInterval)
140   }
141
142   var clearPendingTimeout = function () {
143     if (pendingTimeout) {
144       clearTimeout(pendingTimeout)
145     }
146   }
147
148   // re-glob all the patterns
149   this.refresh = function () {
150     // TODO(vojta): cancel refresh if another refresh starts
151     var buckets = self.buckets = new Array(patterns.length)
152
153     var complete = function () {
154       if (buckets !== self.buckets) {
155         return
156       }
157
158       var files = resolveFiles(buckets)
159
160       resolveDeferred(files)
161       log.debug('Resolved files:\n\t' + files.served.join('\n\t'))
162     }
163
164     // TODO(vojta): use some async helper library for this
165     var pending = 0
166     var finish = function () {
167       pending--
168
169       if (!pending) {
170         complete()
171       }
172     }
173
174     errors = []
175
176     if (!pendingDeferred) {
177       pendingDeferred = q.defer()
178       emitter.emit('file_list_modified', pendingDeferred.promise)
179     }
180
181     clearPendingTimeout()
182
183     patterns.forEach(function (patternObject, i) {
184       var pattern = patternObject.pattern
185
186       if (helper.isUrlAbsolute(pattern)) {
187         buckets[i] = [new Url(pattern)]
188         return
189       }
190
191       pending++
192       glob(pattern, GLOB_OPTS, function (err, resolvedFiles) {
193         if (err) log.warn(err)
194
195         var matchedAndNotIgnored = 0
196
197         buckets[i] = []
198
199         if (!resolvedFiles.length) {
200           log.warn('Pattern "%s" does not match any file.', pattern)
201           return finish()
202         }
203
204         // stat each file to get mtime and isDirectory
205         resolvedFiles.forEach(function (path) {
206           var matchExclude = function (excludePattern) {
207             return mm(path, excludePattern)
208           }
209
210           if (excludes.some(matchExclude)) {
211             log.debug('Excluded file "%s"', path)
212             return
213           }
214
215           pending++
216           matchedAndNotIgnored++
217           fs.stat(path, function (error, stat) {
218             if (error) {
219               log.debug('An error occured while reading "%s"', path)
220               finish()
221             } else {
222               if (!stat.isDirectory()) {
223                 // TODO(vojta): reuse file objects
224                 var file = new File(path, stat.mtime)
225
226                 preprocess(file, function (err) {
227                   buckets[i].push(file)
228
229                   if (err) {
230                     addError(path)
231                   }
232
233                   finish()
234                 })
235               } else {
236                 log.debug('Ignored directory "%s"', path)
237                 finish()
238               }
239             }
240           })
241         })
242
243         if (!matchedAndNotIgnored) {
244           log.warn('All files matched by "%s" were excluded.', pattern)
245         }
246
247         finish()
248       })
249     })
250
251     if (!pending) {
252       process.nextTick(complete)
253     }
254
255     return pendingDeferred.promise
256   }
257
258   // set new patterns and excludes
259   // and re-glob
260   this.reload = function (newPatterns, newExcludes) {
261     patterns = newPatterns
262     excludes = newExcludes
263
264     return this.refresh()
265   }
266
267   /**
268    * Adds a new file into the list (called by watcher)
269    *  - ignore excluded files
270    *  - ignore files that are already in the list
271    *  - get mtime (by stat)
272    *  - fires "file_list_modified"
273    */
274   this.addFile = function (path, done) {
275     var buckets = this.buckets
276     var i, j
277
278     // sorry, this callback is just for easier testing
279     done = done || function () {}
280
281     // check excludes
282     for (i = 0; i < excludes.length; i++) {
283       if (mm(path, excludes[i])) {
284         log.debug('Add file "%s" ignored. Excluded by "%s".', path, excludes[i])
285         return done()
286       }
287     }
288
289     for (i = 0; i < patterns.length; i++) {
290       if (mm(path, patterns[i].pattern)) {
291         for (j = 0; j < buckets[i].length; j++) {
292           if (buckets[i][j].originalPath === path) {
293             log.debug('Add file "%s" ignored. Already in the list.', path)
294             return done()
295           }
296         }
297
298         break
299       }
300     }
301
302     if (i >= patterns.length) {
303       log.debug('Add file "%s" ignored. Does not match any pattern.', path)
304       return done()
305     }
306
307     var addedFile = new File(path)
308     buckets[i].push(addedFile)
309
310     clearPendingTimeout()
311
312     return fs.stat(path, function (err, stat) {
313       if (err) log.warn(err)
314
315       // in the case someone refresh() the list before stat callback
316       if (self.buckets === buckets) {
317         addedFile.mtime = stat.mtime
318
319         return preprocess(addedFile, function (err) {
320           // TODO(vojta): ignore if refresh/reload happens
321           log.info('Added file "%s".', path)
322
323           if (err) {
324             addError(path)
325           }
326
327           fireEventAndDefer()
328           done()
329         })
330       }
331
332       return done()
333     })
334   }
335
336   /**
337    * Update mtime of a file (called by watcher)
338    *  - ignore if file is not in the list
339    *  - ignore if mtime has not changed
340    *  - fire "file_list_modified"
341    */
342   this.changeFile = function (path, done) {
343     var buckets = this.buckets
344
345     // sorry, this callback is just for easier testing
346     done = done || function () {}
347
348     var indices = findBucketByPath(buckets, path) || []
349
350     var i = indices[0]
351     var j = indices[1]
352
353     if (i === undefined || !buckets[i]) {
354       log.debug('Changed file "%s" ignored. Does not match any file in the list.', path)
355       return done()
356     }
357
358     var changedFile = buckets[i][j]
359     return fs.stat(path, function (err, stat) {
360       // https://github.com/paulmillr/chokidar/issues/11
361       if (err || !stat) {
362         return self.removeFile(path, done)
363       }
364
365       if (self.buckets === buckets && stat.mtime > changedFile.mtime) {
366         log.info('Changed file "%s".', path)
367         changedFile.mtime = stat.mtime
368         // TODO(vojta): THIS CAN MAKE FILES INCONSISTENT
369         // if batched change is resolved before preprocessing is finished, the file can be in
370         // inconsistent state, when the promise is resolved.
371         // Solutions:
372         // 1/ the preprocessor should not change the object in place, but create a copy that would
373         // be eventually merged into the original file, here in the callback, synchronously.
374         // 2/ delay the promise resolution - wait for any changeFile operations to finish
375         return preprocess(changedFile, function (err) {
376           if (err) {
377             addError(path)
378           } else {
379             removeError(path)
380           }
381
382           // TODO(vojta): ignore if refresh/reload happens
383           fireEventAndDefer()
384           done()
385         })
386       }
387
388       return done()
389     })
390   }
391
392   /**
393    * Remove a file from the list (called by watcher)
394    *  - ignore if file is not in the list
395    *  - fire "file_list_modified"
396    */
397   this.removeFile = function (path, done) {
398     var buckets = this.buckets
399
400     // sorry, this callback is just for easier testing
401     done = done || function () {}
402
403     for (var i = 0; i < buckets.length; i++) {
404       for (var j = 0; j < buckets[i].length; j++) {
405         if (buckets[i][j].originalPath === path) {
406           buckets[i].splice(j, 1)
407           log.info('Removed file "%s".', path)
408           removeError(path)
409           fireEventAndDefer()
410           return done()
411         }
412       }
413     }
414
415     log.debug('Removed file "%s" ignored. Does not match any file in the list.', path)
416     return done()
417   }
418 }
419 List.$inject = ['config.files', 'config.exclude', 'emitter', 'preprocess',
420   'config.autoWatchBatchDelay']
421
422 // PUBLIC
423 exports.List = List
424 exports.File = File
425 exports.Url = Url