This repository has been archived by the owner on Sep 6, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7.6k
/
FileSystem.js
1034 lines (904 loc) · 42.3 KB
/
FileSystem.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global define */
/**
* FileSystem is a model object representing a complete file system. This object creates
* and manages File and Directory instances, dispatches events when the file system changes,
* and provides methods for showing 'open' and 'save' dialogs.
*
* FileSystem automatically initializes when loaded. It depends on a pluggable "impl" layer, which
* it loads itself but must be designated in the require.config() that loads FileSystem. For details
* see: https://github.com/adobe/brackets/wiki/File-System-Implementations
*
* There are three ways to get File or Directory instances:
* * Use FileSystem.resolve() to convert a path to a File/Directory object. This will only
* succeed if the file/directory already exists.
* * Use FileSystem.getFileForPath()/FileSystem.getDirectoryForPath() if you know the
* file/directory already exists, or if you want to create a new entry.
* * Use Directory.getContents() to return all entries for the specified Directory.
*
* All paths passed *to* FileSystem APIs must be in the following format:
* * The path separator is "/" regardless of platform
* * Paths begin with "/" on Mac/Linux and "c:/" (or some other drive letter) on Windows
*
* All paths returned *from* FileSystem APIs additionally meet the following guarantees:
* * No ".." segments
* * No consecutive "/"s
* * Paths to a directory always end with a trailing "/"
* (Because FileSystem normalizes paths automatically, paths passed *to* FileSystem do not need
* to meet these requirements)
*
* FileSystem dispatches the following events:
* (NOTE: attach to these events via `FileSystem.on()` - not `$(FileSystem).on()`)
*
* __change__ - Sent whenever there is a change in the file system. The handler
* is passed up to three arguments: the changed entry and, if that changed entry
* is a Directory, a list of entries added to the directory and a list of entries
* removed from the Directory. The entry argument can be:
* * a File - the contents of the file have changed, and should be reloaded.
* * a Directory - an immediate child of the directory has been added, removed,
* or renamed/moved. Not triggered for "grandchildren".
* - If the added & removed arguments are null, we don't know what was added/removed:
* clients should assume the whole subtree may have changed.
* - If the added & removed arguments are 0-length, there's no net change in the set
* of files but a file may have been replaced: clients should assume the contents
* of any immediate child file may have changed.
* * null - a 'wholesale' change happened, and you should assume everything may
* have changed.
* For changes made externally, there may be a significant delay before a "change" event
* is dispatched.
*
* __rename__ - Sent whenever a File or Directory is renamed. All affected File and Directory
* objects have been updated to reflect the new path by the time this event is dispatched.
* This event should be used to trigger any UI updates that may need to occur when a path
* has changed. Note that these events will only be sent for rename operations that happen
* within the filesystem. If a file is renamed externally, a change event on the parent
* directory will be sent instead.
*
* FileSystem may perform caching. But it guarantees:
* * File contents & metadata - reads are guaranteed to be up to date (cached data is not used
* without first veryifying it is up to date).
* * Directory structure / file listing - reads may return cached data immediately, which may not
* reflect external changes made recently. (However, changes made via FileSystem itself are always
* reflected immediately, as soon as the change operation's callback signals success).
*
* The FileSystem doesn't directly read or write contents--this work is done by a low-level
* implementation object. This allows client code to use the FileSystem API without having to
* worry about the underlying storage, which could be a local filesystem or a remote server.
*/
define(function (require, exports, module) {
"use strict";
var Directory = require("filesystem/Directory"),
File = require("filesystem/File"),
FileIndex = require("filesystem/FileIndex"),
FileSystemError = require("filesystem/FileSystemError"),
WatchedRoot = require("filesystem/WatchedRoot"),
EventDispatcher = require("utils/EventDispatcher");
/**
* The FileSystem is not usable until init() signals its callback.
* @constructor
*/
function FileSystem() {
// Create a file index
this._index = new FileIndex();
// Initialize the set of watched roots
this._watchedRoots = {};
// Initialize the watch/unwatch request queue
this._watchRequests = [];
// Initialize the queue of pending external changes
this._externalChanges = [];
}
EventDispatcher.makeEventDispatcher(FileSystem.prototype);
/**
* The low-level file system implementation used by this object.
* This is set in the init() function and cannot be changed.
*/
FileSystem.prototype._impl = null;
/**
* The FileIndex used by this object. This is initialized in the constructor.
*/
FileSystem.prototype._index = null;
/**
* Refcount of any pending filesystem mutation operations (e.g., writes,
* unlinks, etc.). Used to ensure that external change events aren't processed
* until after index fixups, operation-specific callbacks, and internal change
* events are complete. (This is important for distinguishing rename from
* an unrelated delete-add pair).
* @type {number}
*/
FileSystem.prototype._activeChangeCount = 0;
// For unit testing only
FileSystem.prototype._getActiveChangeCount = function () {
return this._activeChangeCount;
};
/**
* Queue of arguments with which to invoke _handleExternalChanges(); triggered
* once _activeChangeCount drops to zero.
* @type {!Array.<{path:?string, stat:FileSystemStats=}>}
*/
FileSystem.prototype._externalChanges = null;
/** Process all queued watcher results, by calling _handleExternalChange() on each */
FileSystem.prototype._triggerExternalChangesNow = function () {
this._externalChanges.forEach(function (info) {
this._handleExternalChange(info.path, info.stat);
}, this);
this._externalChanges.length = 0;
};
/**
* Receives a result from the impl's watcher callback, and either processes it
* immediately (if _activeChangeCount is 0) or otherwise stores it for later
* processing.
* @param {?string} path The fullPath of the changed entry
* @param {FileSystemStats=} stat An optional stat object for the changed entry
*/
FileSystem.prototype._enqueueExternalChange = function (path, stat) {
this._externalChanges.push({path: path, stat: stat});
if (!this._activeChangeCount) {
this._triggerExternalChangesNow();
}
};
/**
* The queue of pending watch/unwatch requests.
* @type {Array.<{fn: function(), cb: function()}>}
*/
FileSystem.prototype._watchRequests = null;
/**
* Dequeue and process all pending watch/unwatch requests
*/
FileSystem.prototype._dequeueWatchRequest = function () {
if (this._watchRequests.length > 0) {
var request = this._watchRequests[0];
request.fn.call(null, function () {
// Apply the given callback
var callbackArgs = arguments;
try {
request.cb.apply(null, callbackArgs);
} finally {
// Process the remaining watch/unwatch requests
this._watchRequests.shift();
this._dequeueWatchRequest();
}
}.bind(this));
}
};
/**
* Enqueue a new watch/unwatch request.
*
* @param {function()} fn - The watch/unwatch request function.
* @param {callback()} cb - The callback for the provided watch/unwatch
* request function.
*/
FileSystem.prototype._enqueueWatchRequest = function (fn, cb) {
// Enqueue the given watch/unwatch request
this._watchRequests.push({fn: fn, cb: cb});
// Begin processing the queue if it is not already being processed
if (this._watchRequests.length === 1) {
this._dequeueWatchRequest();
}
};
/**
* The set of watched roots, encoded as a mapping from full paths to WatchedRoot
* objects which contain a file entry, filter function, and an indication of
* whether the watched root is inactive, starting up or fully active.
*
* @type {Object.<string, WatchedRoot>}
*/
FileSystem.prototype._watchedRoots = null;
/**
* Finds a parent watched root for a given path, or returns null if a parent
* watched root does not exist.
*
* @param {string} fullPath The child path for which a parent watched root is to be found
* @return {?{entry: FileSystemEntry, filter: function(string) boolean}} The parent
* watched root, if it exists, or null.
*/
FileSystem.prototype._findWatchedRootForPath = function (fullPath) {
var watchedRoot = null;
Object.keys(this._watchedRoots).some(function (watchedPath) {
if (fullPath.indexOf(watchedPath) === 0) {
watchedRoot = this._watchedRoots[watchedPath];
return true;
}
}, this);
return watchedRoot;
};
/**
* Helper function to watch or unwatch a filesystem entry beneath a given
* watchedRoot.
*
* @private
* @param {FileSystemEntry} entry - The FileSystemEntry to watch. Must be a
* non-strict descendent of watchedRoot.entry.
* @param {WatchedRoot} watchedRoot - See FileSystem._watchedRoots.
* @param {function(?string)} callback - A function that is called once the
* watch is complete, possibly with a FileSystemError string.
* @param {boolean} shouldWatch - Whether the entry should be watched (true)
* or unwatched (false).
*/
FileSystem.prototype._watchOrUnwatchEntry = function (entry, watchedRoot, callback, shouldWatch) {
var impl = this._impl,
recursiveWatch = impl.recursiveWatch,
commandName = shouldWatch ? "watchPath" : "unwatchPath";
if (recursiveWatch) {
// The impl can watch the entire subtree with one call on the root (we also fall into this case for
// unwatch, although that never requires us to do the recursion - see similar final case below)
if (entry !== watchedRoot.entry) {
// Watch and unwatch calls to children of the watched root are
// no-ops if the impl supports recursiveWatch
callback(null);
} else {
// The impl will handle finding all subdirectories to watch.
this._enqueueWatchRequest(function (requestCb) {
impl[commandName].call(impl, entry.fullPath, requestCb);
}.bind(this), callback);
}
} else if (shouldWatch) {
// The impl can't handle recursive watch requests, so it's up to the
// filesystem to recursively watch all subdirectories.
this._enqueueWatchRequest(function (requestCb) {
// First construct a list of entries to watch or unwatch
var entriesToWatch = [];
var visitor = function (child) {
if (watchedRoot.filter(child.name, child.parentPath)) {
if (child.isDirectory || child === watchedRoot.entry) {
entriesToWatch.push(child);
}
return true;
}
return false;
};
entry.visit(visitor, function (err) {
if (err) {
// Unexpected error
requestCb(err);
return;
}
// Then watch or unwatched all these entries
var count = entriesToWatch.length;
if (count === 0) {
requestCb(null);
return;
}
var watchCallback = function () {
if (--count === 0) {
requestCb(null);
}
};
entriesToWatch.forEach(function (entry) {
impl.watchPath(entry.fullPath, watchCallback);
});
});
}, callback);
} else {
// Unwatching never requires enumerating the subfolders (which is good, since after a
// delete/rename we may be unable to do so anyway)
this._enqueueWatchRequest(function (requestCb) {
impl.unwatchPath(entry.fullPath, requestCb);
}, callback);
}
};
/**
* Watch a filesystem entry beneath a given watchedRoot.
*
* @private
* @param {FileSystemEntry} entry - The FileSystemEntry to watch. Must be a
* non-strict descendent of watchedRoot.entry.
* @param {WatchedRoot} watchedRoot - See FileSystem._watchedRoots.
* @param {function(?string)} callback - A function that is called once the
* watch is complete, possibly with a FileSystemError string.
*/
FileSystem.prototype._watchEntry = function (entry, watchedRoot, callback) {
this._watchOrUnwatchEntry(entry, watchedRoot, callback, true);
};
/**
* Unwatch a filesystem entry beneath a given watchedRoot.
*
* @private
* @param {FileSystemEntry} entry - The FileSystemEntry to watch. Must be a
* non-strict descendent of watchedRoot.entry.
* @param {WatchedRoot} watchedRoot - See FileSystem._watchedRoots.
* @param {function(?string)} callback - A function that is called once the
* watch is complete, possibly with a FileSystemError string.
*/
FileSystem.prototype._unwatchEntry = function (entry, watchedRoot, callback) {
this._watchOrUnwatchEntry(entry, watchedRoot, function (err) {
// Make sure to clear cached data for all unwatched entries because
// entries always return cached data if it exists!
this._index.visitAll(function (child) {
if (child.fullPath.indexOf(entry.fullPath) === 0) {
// 'true' so entry doesn't try to clear its immediate childrens' caches too. That would be redundant
// with the visitAll() here, and could be slow if we've already cleared its parent (#7150).
child._clearCachedData(true);
}
}.bind(this));
callback(err);
}.bind(this), false);
};
/**
* Initialize this FileSystem instance.
*
* @param {FileSystemImpl} impl The back-end implementation for this
* FileSystem instance.
*/
FileSystem.prototype.init = function (impl) {
console.assert(!this._impl, "This FileSystem has already been initialized!");
var changeCallback = this._enqueueExternalChange.bind(this),
offlineCallback = this._unwatchAll.bind(this);
this._impl = impl;
this._impl.initWatchers(changeCallback, offlineCallback);
};
/**
* Close a file system. Clear all caches, indexes, and file watchers.
*/
FileSystem.prototype.close = function () {
this._impl.unwatchAll();
this._index.clear();
};
/**
* Returns true if the given path should be automatically added to the index & watch list when one of its ancestors
* is a watch-root. (Files are added automatically when the watch-root is first established, or later when a new
* directory is created and its children enumerated).
*
* Entries explicitly created via FileSystem.getFile/DirectoryForPath() are *always* added to the index regardless
* of this filtering - but they will not be watched if the watch-root's filter excludes them.
*
* @param {string} path Full path
* @param {string} name Name portion of the path
*/
FileSystem.prototype._indexFilter = function (path, name) {
var parentRoot = this._findWatchedRootForPath(path);
if (parentRoot) {
return parentRoot.filter(name, path);
}
// It might seem more sensible to return false (exclude) for files outside the watch roots, but
// that would break usage of appFileSystem for 'system'-level things like enumerating extensions.
// (Or in general, Directory.getContents() for any Directory outside the watch roots).
return true;
};
/**
* Indicates that a filesystem-mutating operation has begun. As long as there
* are changes taking place, change events from the external watchers are
* blocked and queued, to be handled once changes have finished. This is done
* because for mutating operations that originate from within the filesystem,
* synthetic change events are fired that do not depend on external file
* watchers, and we prefer the former over the latter for the following
* reasons: 1) there is no delay; and 2) they may have higher fidelity ---
* e.g., a rename operation can be detected as such, instead of as a nearly
* simultaneous addition and deletion.
*
* All operations that mutate the file system MUST begin with a call to
* _beginChange and must end with a call to _endChange.
*/
FileSystem.prototype._beginChange = function () {
this._activeChangeCount++;
//console.log("> beginChange -> " + this._activeChangeCount);
};
/**
* Indicates that a filesystem-mutating operation has completed. See
* FileSystem._beginChange above.
*/
FileSystem.prototype._endChange = function () {
this._activeChangeCount--;
//console.log("< endChange -> " + this._activeChangeCount);
if (this._activeChangeCount < 0) {
console.error("FileSystem _activeChangeCount has fallen below zero!");
}
if (!this._activeChangeCount) {
this._triggerExternalChangesNow();
}
};
/**
* Determines whether or not the supplied path is absolute, as opposed to relative.
*
* @param {!string} fullPath
* @return {boolean} True if the fullPath is absolute and false otherwise.
*/
FileSystem.isAbsolutePath = function (fullPath) {
return (fullPath[0] === "/" || (fullPath[1] === ":" && fullPath[2] === "/"));
};
function _ensureTrailingSlash(path) {
if (path[path.length - 1] !== "/") {
path += "/";
}
return path;
}
/*
* Matches continguous groups of forward slashes
* @const
*/
var _DUPLICATED_SLASH_RE = /\/{2,}/g;
/**
* Returns a canonical version of the path: no duplicated "/"es, no ".."s,
* and directories guaranteed to end in a trailing "/"
* @param {!string} path Absolute path, using "/" as path separator
* @param {boolean=} isDirectory
* @return {!string}
*/
FileSystem.prototype._normalizePath = function (path, isDirectory) {
if (!FileSystem.isAbsolutePath(path)) {
throw new Error("Paths must be absolute: '" + path + "'"); // expect only absolute paths
}
var isUNCPath = this._impl.normalizeUNCPaths && path.search(_DUPLICATED_SLASH_RE) === 0;
// Remove duplicated "/"es
path = path.replace(_DUPLICATED_SLASH_RE, "/");
// Remove ".." segments
if (path.indexOf("..") !== -1) {
var segments = path.split("/"),
i;
for (i = 1; i < segments.length; i++) {
if (segments[i] === "..") {
if (i < 2) {
throw new Error("Invalid absolute path: '" + path + "'");
}
segments.splice(i - 1, 2);
i -= 2; // compensate so we start on the right index next iteration
}
}
path = segments.join("/");
}
if (isDirectory) {
// Make sure path DOES include trailing slash
path = _ensureTrailingSlash(path);
}
if (isUNCPath) {
// Restore the leading double slash that was removed previously
path = "/" + path;
}
return path;
};
/**
* This method adds an entry for a file in the file Index. Files on disk are added
* to the file index either on load or on open. This method is primarily needed to add
* in memory files to the index
*
* @param {File} The fileEntry which needs to be added
* @param {String} The full path to the file
*/
FileSystem.prototype.addEntryForPathIfRequired = function (fileEntry, path) {
var entry = this._index.getEntry(path);
if (!entry) {
this._index.addEntry(fileEntry);
}
};
/**
* Return a (strict subclass of a) FileSystemEntry object for the specified
* path using the provided constuctor. For now, the provided constructor
* should be either File or Directory.
*
* @private
* @param {function(string, FileSystem)} EntryConstructor Constructor with
* which to initialize new FileSystemEntry objects.
* @param {string} path Absolute path of file.
* @return {File|Directory} The File or Directory object. This file may not
* yet exist on disk.
*/
FileSystem.prototype._getEntryForPath = function (EntryConstructor, path) {
var isDirectory = EntryConstructor === Directory;
path = this._normalizePath(path, isDirectory);
var entry = this._index.getEntry(path);
if (!entry) {
entry = new EntryConstructor(path, this);
this._index.addEntry(entry);
}
return entry;
};
/**
* Return a File object for the specified path.
*
* @param {string} path Absolute path of file.
*
* @return {File} The File object. This file may not yet exist on disk.
*/
FileSystem.prototype.getFileForPath = function (path) {
return this._getEntryForPath(File, path);
};
/**
* Return a Directory object for the specified path.
*
* @param {string} path Absolute path of directory.
*
* @return {Directory} The Directory object. This directory may not yet exist on disk.
*/
FileSystem.prototype.getDirectoryForPath = function (path) {
return this._getEntryForPath(Directory, path);
};
/**
* Resolve a path.
*
* @param {string} path The path to resolve
* @param {function (?string, FileSystemEntry=, FileSystemStats=)} callback Callback resolved
* with a FileSystemError string or with the entry for the provided path.
*/
FileSystem.prototype.resolve = function (path, callback) {
var normalizedPath = this._normalizePath(path, false),
item = this._index.getEntry(normalizedPath);
if (!item) {
normalizedPath = _ensureTrailingSlash(normalizedPath);
item = this._index.getEntry(normalizedPath);
}
if (item) {
item.stat(function (err, stat) {
if (err) {
callback(err);
return;
}
callback(null, item, stat);
});
} else {
this._impl.stat(path, function (err, stat) {
if (err) {
callback(err);
return;
}
if (stat.isFile) {
item = this.getFileForPath(path);
} else {
item = this.getDirectoryForPath(path);
}
if (item._isWatched()) {
item._stat = stat;
}
callback(null, item, stat);
}.bind(this));
}
};
/**
* Show an "Open" dialog and return the file(s)/directories selected by the user.
*
* @param {boolean} allowMultipleSelection Allows selecting more than one file at a time
* @param {boolean} chooseDirectories Allows directories to be opened
* @param {string} title The title of the dialog
* @param {string} initialPath The folder opened inside the window initially. If initialPath
* is not set, or it doesn't exist, the window would show the last
* browsed folder depending on the OS preferences
* @param {?Array.<string>} fileTypes (Currently *ignored* except on Mac - https://trello.com/c/430aXkpq)
* List of extensions that are allowed to be opened, without leading ".".
* Null or empty array allows all files to be selected. Not applicable
* when chooseDirectories = true.
* @param {function (?string, Array.<string>=)} callback Callback resolved with a FileSystemError
* string or the selected file(s)/directories. If the user cancels the
* open dialog, the error will be falsy and the file/directory array will
* be empty.
*/
FileSystem.prototype.showOpenDialog = function (allowMultipleSelection,
chooseDirectories,
title,
initialPath,
fileTypes,
callback) {
this._impl.showOpenDialog(allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, callback);
};
/**
* Show a "Save" dialog and return the path of the file to save.
*
* @param {string} title The title of the dialog.
* @param {string} initialPath The folder opened inside the window initially. If initialPath
* is not set, or it doesn't exist, the window would show the last
* browsed folder depending on the OS preferences.
* @param {string} proposedNewFilename Provide a new file name for the user. This could be based on
* on the current file name plus an additional suffix
* @param {function (?string, string=)} callback Callback that is resolved with a FileSystemError
* string or the name of the file to save. If the user cancels the save,
* the error will be falsy and the name will be empty.
*/
FileSystem.prototype.showSaveDialog = function (title, initialPath, proposedNewFilename, callback) {
this._impl.showSaveDialog(title, initialPath, proposedNewFilename, callback);
};
/**
* Fire a rename event. Clients listen for these events using FileSystem.on.
*
* @param {string} oldPath The entry's previous fullPath
* @param {string} newPath The entry's current fullPath
*/
FileSystem.prototype._fireRenameEvent = function (oldPath, newPath) {
this.trigger("rename", oldPath, newPath);
};
/**
* Fire a change event. Clients listen for these events using FileSystem.on.
*
* @param {File|Directory} entry The entry that has changed
* @param {Array<File|Directory>=} added If the entry is a directory, this
* is a set of new entries in the directory.
* @param {Array<File|Directory>=} removed If the entry is a directory, this
* is a set of removed entries from the directory.
*/
FileSystem.prototype._fireChangeEvent = function (entry, added, removed) {
this.trigger("change", entry, added, removed);
};
/**
* @private
* Notify the system when an entry name has changed.
*
* @param {string} oldFullPath
* @param {string} newFullPath
* @param {boolean} isDirectory
*/
FileSystem.prototype._handleRename = function (oldFullPath, newFullPath, isDirectory) {
// Update all affected entries in the index
this._index.entryRenamed(oldFullPath, newFullPath, isDirectory);
};
/**
* Notify the filesystem that the given directory has changed. Updates the filesystem's
* internal state as a result of the change, and calls back with the set of added and
* removed entries. Mutating FileSystemEntry operations should call this method before
* applying the operation's callback, and pass along the resulting change sets in the
* internal change event.
*
* @param {Directory} directory The directory that has changed.
* @param {function(Array<File|Directory>=, Array<File|Directory>=)} callback
* The callback that will be applied to a set of added and a set of removed
* FileSystemEntry objects.
*/
FileSystem.prototype._handleDirectoryChange = function (directory, callback) {
var oldContents = directory._contents;
directory._clearCachedData();
directory.getContents(function (err, contents) {
var addedEntries = oldContents && contents.filter(function (entry) {
return oldContents.indexOf(entry) === -1;
});
var removedEntries = oldContents && oldContents.filter(function (entry) {
return contents.indexOf(entry) === -1;
});
// If directory is not watched, clear children's caches manually.
var watchedRoot = this._findWatchedRootForPath(directory.fullPath);
if (!watchedRoot || !watchedRoot.filter(directory.name, directory.parentPath)) {
this._index.visitAll(function (entry) {
if (entry.fullPath.indexOf(directory.fullPath) === 0) {
// Passing 'true' for a similar reason as in _unwatchEntry() - see #7150
entry._clearCachedData(true);
}
}.bind(this));
callback(addedEntries, removedEntries);
return;
}
var addedCounter = addedEntries ? addedEntries.length : 0,
removedCounter = removedEntries ? removedEntries.length : 0,
counter = addedCounter + removedCounter;
if (counter === 0) {
callback(addedEntries, removedEntries);
return;
}
var watchOrUnwatchCallback = function (err) {
if (err) {
console.error("FileSystem error in _handleDirectoryChange after watch/unwatch entries: " + err);
}
if (--counter === 0) {
callback(addedEntries, removedEntries);
}
};
if (addedEntries) {
addedEntries.forEach(function (entry) {
this._watchEntry(entry, watchedRoot, watchOrUnwatchCallback);
}, this);
}
if (removedEntries) {
removedEntries.forEach(function (entry) {
this._unwatchEntry(entry, watchedRoot, watchOrUnwatchCallback);
}, this);
}
}.bind(this));
};
/**
* @private
* Processes a result from the file/directory watchers. Watch results are sent from the low-level implementation
* whenever a directory or file is changed.
*
* @param {string} path The path that changed. This could be a file or a directory.
* @param {FileSystemStats=} stat Optional stat for the item that changed. This param is not always
* passed.
*/
FileSystem.prototype._handleExternalChange = function (path, stat) {
if (!path) {
// This is a "wholesale" change event; clear all caches
this._index.visitAll(function (entry) {
// Passing 'true' for a similar reason as in _unwatchEntry() - see #7150
entry._clearCachedData(true);
});
this._fireChangeEvent(null);
return;
}
path = this._normalizePath(path, false);
var entry = this._index.getEntry(path);
if (entry) {
var oldStat = entry._stat;
if (entry.isFile) {
// Update stat and clear contents, but only if out of date
if (!(stat && oldStat && stat.mtime.getTime() === oldStat.mtime.getTime())) {
entry._clearCachedData();
entry._stat = stat;
this._fireChangeEvent(entry);
}
} else {
this._handleDirectoryChange(entry, function (added, removed) {
entry._stat = stat;
if (entry._isWatched()) {
// We send a change even if added & removed are both zero-length. Something may still have changed,
// e.g. a file may have been quickly removed & re-added before we got a chance to reread the directory
// listing.
this._fireChangeEvent(entry, added, removed);
}
}.bind(this));
}
}
};
/**
* Clears all cached content. Because of the performance implications of this, this should only be used if
* there is a suspicion that the file system has not been updated through the normal file watchers
* mechanism.
*/
FileSystem.prototype.clearAllCaches = function () {
this._handleExternalChange(null);
};
/**
* Start watching a filesystem root entry.
*
* @param {FileSystemEntry} entry - The root entry to watch. If entry is a directory,
* all subdirectories that aren't explicitly filtered will also be watched.
* @param {function(string): boolean} filter - Returns true if a particular item should
* be watched, given its name (not full path). Items that are ignored are also
* filtered from Directory.getContents() results within this subtree.
* @param {function(?string)=} callback - A function that is called when the watch has
* completed. If the watch fails, the function will have a non-null FileSystemError
* string parametr.
*/
FileSystem.prototype.watch = function (entry, filter, callback) {
var fullPath = entry.fullPath;
callback = callback || function () {};
var watchingParentRoot = this._findWatchedRootForPath(fullPath);
if (watchingParentRoot &&
(watchingParentRoot.status === WatchedRoot.STARTING ||
watchingParentRoot.status === WatchedRoot.ACTIVE)) {
callback("A parent of this root is already watched");
return;
}
var watchingChildRoot = Object.keys(this._watchedRoots).some(function (path) {
var watchedRoot = this._watchedRoots[path],
watchedPath = watchedRoot.entry.fullPath;
return watchedPath.indexOf(fullPath) === 0;
}, this);
if (watchingChildRoot &&
(watchingChildRoot.status === WatchedRoot.STARTING ||
watchingChildRoot.status === WatchedRoot.ACTIVE)) {
callback("A child of this root is already watched");
return;
}
var watchedRoot = new WatchedRoot(entry, filter);
this._watchedRoots[fullPath] = watchedRoot;
// Enter the STARTING state early to indiate that watched Directory
// objects may cache their contents. See FileSystemEntry._isWatched.
watchedRoot.status = WatchedRoot.STARTING;
this._watchEntry(entry, watchedRoot, function (err) {
if (err) {
console.warn("Failed to watch root: ", entry.fullPath, err);
delete this._watchedRoots[fullPath];
callback(err);
return;
}
watchedRoot.status = WatchedRoot.ACTIVE;
callback(null);
}.bind(this));
};
/**
* Stop watching a filesystem root entry.
*
* @param {FileSystemEntry} entry - The root entry to stop watching. The unwatch will
* if the entry is not currently being watched.
* @param {function(?string)=} callback - A function that is called when the unwatch has
* completed. If the unwatch fails, the function will have a non-null FileSystemError
* string parameter.
*/
FileSystem.prototype.unwatch = function (entry, callback) {
var fullPath = entry.fullPath,
watchedRoot = this._watchedRoots[fullPath];
callback = callback || function () {};
if (!watchedRoot) {
callback(FileSystemError.ROOT_NOT_WATCHED);
return;
}
// Mark this as inactive, but don't delete the entry until the unwatch is complete.
// This is useful for making sure we don't try to concurrently watch overlapping roots.
watchedRoot.status = WatchedRoot.INACTIVE;
this._unwatchEntry(entry, watchedRoot, function (err) {
delete this._watchedRoots[fullPath];
this._index.visitAll(function (child) {
if (child.fullPath.indexOf(entry.fullPath) === 0) {
this._index.removeEntry(child);
}
}.bind(this));
if (err) {
console.warn("Failed to unwatch root: ", entry.fullPath, err);
callback(err);
return;
}
callback(null);
}.bind(this));
};
/**
* Unwatch all watched roots. Calls unwatch on the underlying impl for each
* watched root and ignores errors.
* @private
*/
FileSystem.prototype._unwatchAll = function () {
console.warn("File watchers went offline!");
Object.keys(this._watchedRoots).forEach(function (path) {
var watchedRoot = this._watchedRoots[path];
watchedRoot.status = WatchedRoot.INACTIVE;
delete this._watchedRoots[path];
this._unwatchEntry(watchedRoot.entry, watchedRoot, function () {
console.warn("Watching disabled for", watchedRoot.entry.fullPath);
});
}, this);
// Fire a wholesale change event, clearing all caches and request that
// clients manually update their state.
this._handleExternalChange(null);
};
// The singleton instance
var _instance;
function _wrap(func) {
return function () {
return func.apply(_instance, arguments);
};
}
// Export public methods as proxies to the singleton instance
exports.init = _wrap(FileSystem.prototype.init);
exports.close = _wrap(FileSystem.prototype.close);
exports.shouldShow = _wrap(FileSystem.prototype.shouldShow);
exports.getFileForPath = _wrap(FileSystem.prototype.getFileForPath);
exports.addEntryForPathIfRequired = _wrap(FileSystem.prototype.addEntryForPathIfRequired);
exports.getDirectoryForPath = _wrap(FileSystem.prototype.getDirectoryForPath);
exports.resolve = _wrap(FileSystem.prototype.resolve);
exports.showOpenDialog = _wrap(FileSystem.prototype.showOpenDialog);
exports.showSaveDialog = _wrap(FileSystem.prototype.showSaveDialog);
exports.watch = _wrap(FileSystem.prototype.watch);
exports.unwatch = _wrap(FileSystem.prototype.unwatch);
exports.clearAllCaches = _wrap(FileSystem.prototype.clearAllCaches);
// Static public utility methods