OLD | NEW |
| (Empty) |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library memory_file_system; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:collection'; | |
9 import 'dart:core' hide Resource; | |
10 | |
11 import 'package:analyzer/src/generated/engine.dart' show TimestampedData; | |
12 import 'package:analyzer/src/generated/source_io.dart'; | |
13 import 'package:path/path.dart'; | |
14 import 'package:watcher/watcher.dart'; | |
15 | |
16 import 'file_system.dart'; | |
17 | |
18 /** | |
19 * An in-memory implementation of [ResourceProvider]. | |
20 * Use `/` as a path separator. | |
21 */ | |
22 class MemoryResourceProvider implements ResourceProvider { | |
23 final Map<String, _MemoryResource> _pathToResource = | |
24 new HashMap<String, _MemoryResource>(); | |
25 final Map<String, String> _pathToContent = new HashMap<String, String>(); | |
26 final Map<String, int> _pathToTimestamp = new HashMap<String, int>(); | |
27 final Map<String, List<StreamController<WatchEvent>>> _pathToWatchers = | |
28 new HashMap<String, List<StreamController<WatchEvent>>>(); | |
29 int nextStamp = 0; | |
30 | |
31 @override | |
32 Context get pathContext => posix; | |
33 | |
34 /** | |
35 * Delete the file with the given path. | |
36 */ | |
37 void deleteFile(String path) { | |
38 _checkFileAtPath(path); | |
39 _pathToResource.remove(path); | |
40 _pathToContent.remove(path); | |
41 _pathToTimestamp.remove(path); | |
42 _notifyWatchers(path, ChangeType.REMOVE); | |
43 } | |
44 | |
45 /** | |
46 * Delete the folder with the given path | |
47 * and recurively delete nested files and folders. | |
48 */ | |
49 void deleteFolder(String path) { | |
50 _checkFolderAtPath(path); | |
51 _MemoryFolder folder = _pathToResource[path]; | |
52 for (Resource child in folder.getChildren()) { | |
53 if (child is File) { | |
54 deleteFile(child.path); | |
55 } else if (child is Folder) { | |
56 deleteFolder(child.path); | |
57 } else { | |
58 throw 'failed to delete resource: $child'; | |
59 } | |
60 } | |
61 _pathToResource.remove(path); | |
62 _pathToContent.remove(path); | |
63 _pathToTimestamp.remove(path); | |
64 _notifyWatchers(path, ChangeType.REMOVE); | |
65 } | |
66 | |
67 @override | |
68 File getFile(String path) => new _MemoryFile(this, path); | |
69 | |
70 @override | |
71 Folder getFolder(String path) => newFolder(path); | |
72 | |
73 @override | |
74 Resource getResource(String path) { | |
75 path = posix.normalize(path); | |
76 Resource resource = _pathToResource[path]; | |
77 if (resource == null) { | |
78 resource = new _MemoryFile(this, path); | |
79 } | |
80 return resource; | |
81 } | |
82 | |
83 @override | |
84 Folder getStateLocation(String pluginId) { | |
85 return newFolder('/user/home/$pluginId'); | |
86 } | |
87 | |
88 void modifyFile(String path, String content) { | |
89 _checkFileAtPath(path); | |
90 _pathToContent[path] = content; | |
91 _pathToTimestamp[path] = nextStamp++; | |
92 _notifyWatchers(path, ChangeType.MODIFY); | |
93 } | |
94 | |
95 /** | |
96 * Create a resource representing a dummy link (that is, a File object which | |
97 * appears in its parent directory, but whose `exists` property is false) | |
98 */ | |
99 File newDummyLink(String path) { | |
100 path = posix.normalize(path); | |
101 newFolder(posix.dirname(path)); | |
102 _MemoryDummyLink link = new _MemoryDummyLink(this, path); | |
103 _pathToResource[path] = link; | |
104 _pathToTimestamp[path] = nextStamp++; | |
105 _notifyWatchers(path, ChangeType.ADD); | |
106 return link; | |
107 } | |
108 | |
109 File newFile(String path, String content, [int stamp]) { | |
110 path = posix.normalize(path); | |
111 newFolder(posix.dirname(path)); | |
112 _MemoryFile file = new _MemoryFile(this, path); | |
113 _pathToResource[path] = file; | |
114 _pathToContent[path] = content; | |
115 _pathToTimestamp[path] = stamp != null ? stamp : nextStamp++; | |
116 _notifyWatchers(path, ChangeType.ADD); | |
117 return file; | |
118 } | |
119 | |
120 Folder newFolder(String path) { | |
121 path = posix.normalize(path); | |
122 if (!path.startsWith('/')) { | |
123 throw new ArgumentError("Path must start with '/'"); | |
124 } | |
125 _MemoryResource resource = _pathToResource[path]; | |
126 if (resource == null) { | |
127 String parentPath = posix.dirname(path); | |
128 if (parentPath != path) { | |
129 newFolder(parentPath); | |
130 } | |
131 _MemoryFolder folder = new _MemoryFolder(this, path); | |
132 _pathToResource[path] = folder; | |
133 _pathToTimestamp[path] = nextStamp++; | |
134 return folder; | |
135 } else if (resource is _MemoryFolder) { | |
136 return resource; | |
137 } else { | |
138 String message = | |
139 'Folder expected at ' "'$path'" 'but ${resource.runtimeType} found'; | |
140 throw new ArgumentError(message); | |
141 } | |
142 } | |
143 | |
144 File updateFile(String path, String content, [int stamp]) { | |
145 path = posix.normalize(path); | |
146 newFolder(posix.dirname(path)); | |
147 _MemoryFile file = new _MemoryFile(this, path); | |
148 _pathToResource[path] = file; | |
149 _pathToContent[path] = content; | |
150 _pathToTimestamp[path] = stamp != null ? stamp : nextStamp++; | |
151 _notifyWatchers(path, ChangeType.MODIFY); | |
152 return file; | |
153 } | |
154 | |
155 void _checkFileAtPath(String path) { | |
156 _MemoryResource resource = _pathToResource[path]; | |
157 if (resource is! _MemoryFile) { | |
158 throw new ArgumentError( | |
159 'File expected at "$path" but ${resource.runtimeType} found'); | |
160 } | |
161 } | |
162 | |
163 void _checkFolderAtPath(String path) { | |
164 _MemoryResource resource = _pathToResource[path]; | |
165 if (resource is! _MemoryFolder) { | |
166 throw new ArgumentError( | |
167 'Folder expected at "$path" but ${resource.runtimeType} found'); | |
168 } | |
169 } | |
170 | |
171 void _notifyWatchers(String path, ChangeType changeType) { | |
172 _pathToWatchers.forEach((String watcherPath, | |
173 List<StreamController<WatchEvent>> streamControllers) { | |
174 if (watcherPath == path || posix.isWithin(watcherPath, path)) { | |
175 for (StreamController<WatchEvent> streamController | |
176 in streamControllers) { | |
177 streamController.add(new WatchEvent(changeType, path)); | |
178 } | |
179 } | |
180 }); | |
181 } | |
182 } | |
183 | |
184 /** | |
185 * An in-memory implementation of [File] which acts like a symbolic link to a | |
186 * non-existent file. | |
187 */ | |
188 class _MemoryDummyLink extends _MemoryResource implements File { | |
189 _MemoryDummyLink(MemoryResourceProvider provider, String path) | |
190 : super(provider, path); | |
191 | |
192 @override | |
193 Stream<WatchEvent> get changes { | |
194 throw new FileSystemException(path, "File does not exist"); | |
195 } | |
196 | |
197 @override | |
198 bool get exists => false; | |
199 | |
200 int get modificationStamp { | |
201 int stamp = _provider._pathToTimestamp[path]; | |
202 if (stamp == null) { | |
203 throw new FileSystemException(path, "File does not exist"); | |
204 } | |
205 return stamp; | |
206 } | |
207 | |
208 String get _content { | |
209 throw new FileSystemException(path, 'File could not be read'); | |
210 } | |
211 | |
212 @override | |
213 Source createSource([Uri uri]) { | |
214 throw new FileSystemException(path, 'File could not be read'); | |
215 } | |
216 | |
217 @override | |
218 bool isOrContains(String path) { | |
219 return path == this.path; | |
220 } | |
221 | |
222 @override | |
223 String readAsStringSync() { | |
224 throw new FileSystemException(path, 'File could not be read'); | |
225 } | |
226 } | |
227 | |
228 /** | |
229 * An in-memory implementation of [File]. | |
230 */ | |
231 class _MemoryFile extends _MemoryResource implements File { | |
232 _MemoryFile(MemoryResourceProvider provider, String path) | |
233 : super(provider, path); | |
234 | |
235 @override | |
236 bool get exists => _provider._pathToResource[path] is _MemoryFile; | |
237 | |
238 int get modificationStamp { | |
239 int stamp = _provider._pathToTimestamp[path]; | |
240 if (stamp == null) { | |
241 throw new FileSystemException(path, 'File "$path" does not exist.'); | |
242 } | |
243 return stamp; | |
244 } | |
245 | |
246 String get _content { | |
247 String content = _provider._pathToContent[path]; | |
248 if (content == null) { | |
249 throw new FileSystemException(path, 'File "$path" does not exist.'); | |
250 } | |
251 return content; | |
252 } | |
253 | |
254 @override | |
255 Source createSource([Uri uri]) { | |
256 if (uri == null) { | |
257 uri = posix.toUri(path); | |
258 } | |
259 return new _MemoryFileSource(this, uri); | |
260 } | |
261 | |
262 @override | |
263 bool isOrContains(String path) { | |
264 return path == this.path; | |
265 } | |
266 | |
267 @override | |
268 String readAsStringSync() { | |
269 String content = _provider._pathToContent[path]; | |
270 if (content == null) { | |
271 throw new FileSystemException(path, 'File "$path" does not exist.'); | |
272 } | |
273 return content; | |
274 } | |
275 } | |
276 | |
277 /** | |
278 * An in-memory implementation of [Source]. | |
279 */ | |
280 class _MemoryFileSource extends Source { | |
281 /** | |
282 * Map from encoded URI/filepath pair to a unique integer identifier. This | |
283 * identifier is used for equality tests and hash codes. | |
284 * | |
285 * The URI and filepath are joined into a pair by separating them with an '@' | |
286 * character. | |
287 */ | |
288 static final Map<String, int> _idTable = new HashMap<String, int>(); | |
289 | |
290 final _MemoryFile file; | |
291 | |
292 final Uri uri; | |
293 | |
294 /** | |
295 * The unique ID associated with this [_MemoryFileSource]. | |
296 */ | |
297 final int id; | |
298 | |
299 _MemoryFileSource(_MemoryFile file, Uri uri) | |
300 : uri = uri, | |
301 file = file, | |
302 id = _idTable.putIfAbsent('$uri@${file.path}', () => _idTable.length); | |
303 | |
304 @override | |
305 TimestampedData<String> get contents { | |
306 return new TimestampedData<String>(modificationStamp, file._content); | |
307 } | |
308 | |
309 @override | |
310 String get encoding { | |
311 return uri.toString(); | |
312 } | |
313 | |
314 @override | |
315 String get fullName => file.path; | |
316 | |
317 @override | |
318 int get hashCode => id; | |
319 | |
320 @override | |
321 bool get isInSystemLibrary => uriKind == UriKind.DART_URI; | |
322 | |
323 @override | |
324 int get modificationStamp { | |
325 try { | |
326 return file.modificationStamp; | |
327 } on FileSystemException { | |
328 return -1; | |
329 } | |
330 } | |
331 | |
332 @override | |
333 String get shortName => file.shortName; | |
334 | |
335 @override | |
336 UriKind get uriKind { | |
337 String scheme = uri.scheme; | |
338 if (scheme == PackageUriResolver.PACKAGE_SCHEME) { | |
339 return UriKind.PACKAGE_URI; | |
340 } else if (scheme == DartUriResolver.DART_SCHEME) { | |
341 return UriKind.DART_URI; | |
342 } else if (scheme == FileUriResolver.FILE_SCHEME) { | |
343 return UriKind.FILE_URI; | |
344 } | |
345 return UriKind.FILE_URI; | |
346 } | |
347 | |
348 @override | |
349 bool operator ==(other) { | |
350 return other is _MemoryFileSource && other.id == id; | |
351 } | |
352 | |
353 @override | |
354 bool exists() => file.exists; | |
355 | |
356 @override | |
357 Uri resolveRelativeUri(Uri relativeUri) { | |
358 return uri.resolveUri(relativeUri); | |
359 } | |
360 | |
361 @override | |
362 String toString() => file.toString(); | |
363 } | |
364 | |
365 /** | |
366 * An in-memory implementation of [Folder]. | |
367 */ | |
368 class _MemoryFolder extends _MemoryResource implements Folder { | |
369 _MemoryFolder(MemoryResourceProvider provider, String path) | |
370 : super(provider, path); | |
371 | |
372 @override | |
373 bool get exists => _provider._pathToResource[path] is _MemoryFolder; | |
374 | |
375 @override | |
376 String canonicalizePath(String relPath) { | |
377 relPath = posix.normalize(relPath); | |
378 String childPath = posix.join(path, relPath); | |
379 childPath = posix.normalize(childPath); | |
380 return childPath; | |
381 } | |
382 | |
383 @override | |
384 bool contains(String path) { | |
385 return posix.isWithin(this.path, path); | |
386 } | |
387 | |
388 @override | |
389 Resource getChild(String relPath) { | |
390 String childPath = canonicalizePath(relPath); | |
391 _MemoryResource resource = _provider._pathToResource[childPath]; | |
392 if (resource == null) { | |
393 resource = new _MemoryFile(_provider, childPath); | |
394 } | |
395 return resource; | |
396 } | |
397 | |
398 @override | |
399 _MemoryFolder getChildAssumingFolder(String relPath) { | |
400 String childPath = canonicalizePath(relPath); | |
401 _MemoryResource resource = _provider._pathToResource[childPath]; | |
402 if (resource is _MemoryFolder) { | |
403 return resource; | |
404 } | |
405 return new _MemoryFolder(_provider, childPath); | |
406 } | |
407 | |
408 @override | |
409 List<Resource> getChildren() { | |
410 if (!exists) { | |
411 throw new FileSystemException(path, 'Folder does not exist.'); | |
412 } | |
413 List<Resource> children = <Resource>[]; | |
414 _provider._pathToResource.forEach((resourcePath, resource) { | |
415 if (posix.dirname(resourcePath) == path) { | |
416 children.add(resource); | |
417 } | |
418 }); | |
419 return children; | |
420 } | |
421 | |
422 @override | |
423 bool isOrContains(String path) { | |
424 if (path == this.path) { | |
425 return true; | |
426 } | |
427 return contains(path); | |
428 } | |
429 } | |
430 | |
431 /** | |
432 * An in-memory implementation of [Resource]. | |
433 */ | |
434 abstract class _MemoryResource implements Resource { | |
435 final MemoryResourceProvider _provider; | |
436 final String path; | |
437 | |
438 _MemoryResource(this._provider, this.path); | |
439 | |
440 Stream<WatchEvent> get changes { | |
441 StreamController<WatchEvent> streamController = | |
442 new StreamController<WatchEvent>(); | |
443 if (!_provider._pathToWatchers.containsKey(path)) { | |
444 _provider._pathToWatchers[path] = <StreamController<WatchEvent>>[]; | |
445 } | |
446 _provider._pathToWatchers[path].add(streamController); | |
447 streamController.done.then((_) { | |
448 _provider._pathToWatchers[path].remove(streamController); | |
449 if (_provider._pathToWatchers[path].isEmpty) { | |
450 _provider._pathToWatchers.remove(path); | |
451 } | |
452 }); | |
453 return streamController.stream; | |
454 } | |
455 | |
456 @override | |
457 get hashCode => path.hashCode; | |
458 | |
459 @override | |
460 Folder get parent { | |
461 String parentPath = posix.dirname(path); | |
462 if (parentPath == path) { | |
463 return null; | |
464 } | |
465 return _provider.getResource(parentPath); | |
466 } | |
467 | |
468 @override | |
469 String get shortName => posix.basename(path); | |
470 | |
471 @override | |
472 bool operator ==(other) { | |
473 if (runtimeType != other.runtimeType) { | |
474 return false; | |
475 } | |
476 return path == other.path; | |
477 } | |
478 | |
479 @override | |
480 String toString() => path; | |
481 } | |
OLD | NEW |