be4df1a869
This aligns names with ResourceProvider.getFile/Folder. Change-Id: I30383ef1fa6f7cbe60b187338e25b8ca75806730 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/511120 Reviewed-by: Johnni Winther <johnniwinther@google.com> Reviewed-by: Jonas Jensen <jonasfj@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
682 lines
19 KiB
Dart
682 lines
19 KiB
Dart
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:analyzer/file_system/file_system.dart';
|
|
import 'package:meta/meta.dart';
|
|
import 'package:path/path.dart' as pathos;
|
|
import 'package:watcher/watcher.dart' hide Watcher;
|
|
|
|
/// An in-memory implementation of [ResourceProvider].
|
|
/// Use `/` as a path separator.
|
|
class MemoryResourceProvider implements ResourceProvider {
|
|
final Map<String, _ResourceData> _pathToData = {};
|
|
final Map<String, String> _pathToLinkedPath = {};
|
|
final Map<String, List<StreamController<WatchEvent>>> _pathToWatchers = {};
|
|
int nextStamp = 0;
|
|
|
|
final pathos.Context _pathContext;
|
|
|
|
/// An artificial delay that's waited when initializing watchers.
|
|
///
|
|
/// This allows mirroring how the real fs watcher works, where events may be
|
|
/// lost between creating the watcher and its `ready` event firing.
|
|
///
|
|
/// Like the real file watcher, the `ready` event also requires a listener
|
|
/// to be attached before it will fire.
|
|
@visibleForTesting
|
|
final Duration? delayWatcherInitialization;
|
|
|
|
/// Paths that should have `PathNotFoundException`s emitted on their watch
|
|
/// streams.
|
|
@visibleForTesting
|
|
final Set<String> emitPathNotFoundExceptionsForPaths = {};
|
|
|
|
MemoryResourceProvider({
|
|
pathos.Context? context,
|
|
this.delayWatcherInitialization,
|
|
}) : _pathContext = context ??= pathos.style == pathos.Style.windows
|
|
// On Windows, ensure that the current drive matches
|
|
// the drive inserted by ResourceProvider.convertPath
|
|
// so that packages are mapped to the correct drive
|
|
? pathos.Context(current: 'C:\\')
|
|
: pathos.context;
|
|
|
|
@override
|
|
pathos.Context get pathContext => _pathContext;
|
|
|
|
/// Converts the given posix [filePath] to conform to this provider's path
|
|
/// context.
|
|
///
|
|
/// This is a utility method for testing; paths passed in to other methods in
|
|
/// this class are never converted automatically.
|
|
@Deprecated("Use 'ResourceProviderExtensions.convertPath' directly")
|
|
String convertPath(String filePath) {
|
|
// This implementation is duplicate with that at
|
|
// 'package:analyzer_testing/utilities/extensions/resource_provider.dart'.
|
|
// But the analyzer package's lib code _cannot_ depend on the
|
|
// analyzer_testing package; hence this duplication until this test-only
|
|
// method is removed.
|
|
if (pathContext.style == pathos.windows.style) {
|
|
if (filePath.startsWith(pathos.posix.separator)) {
|
|
filePath = r'C:' + filePath;
|
|
}
|
|
filePath = filePath.replaceAll(
|
|
pathos.posix.separator,
|
|
pathos.windows.separator,
|
|
);
|
|
}
|
|
return filePath;
|
|
}
|
|
|
|
/// Delete the file with the given path.
|
|
void deleteFile(String path) {
|
|
var data = _pathToData[path];
|
|
if (data is! _FileData) {
|
|
throw FileSystemException(path, 'Not a file.');
|
|
}
|
|
|
|
_pathToData.remove(path);
|
|
_removeFromParentFolderData(path);
|
|
|
|
_notifyWatchers(path, ChangeType.REMOVE);
|
|
}
|
|
|
|
/// Delete the folder with the given path
|
|
/// and recursively delete nested files and folders.
|
|
void deleteFolder(String path) {
|
|
var data = _pathToData[path];
|
|
if (data is! _FolderData) {
|
|
throw FileSystemException(path, 'Not a folder.');
|
|
}
|
|
|
|
for (var childName in data.childNames.toList()) {
|
|
var childPath = pathContext.join(path, childName);
|
|
var child = getResource(childPath);
|
|
if (child is File) {
|
|
deleteFile(child.path);
|
|
} else if (child is Folder) {
|
|
deleteFolder(child.path);
|
|
} else {
|
|
throw 'failed to delete resource: $child';
|
|
}
|
|
}
|
|
|
|
if (_pathToData[path] != data) {
|
|
throw StateError('Unexpected concurrent modification: $path');
|
|
}
|
|
if (data.childNames.isNotEmpty) {
|
|
throw StateError('Must be empty.');
|
|
}
|
|
|
|
_pathToData.remove(path);
|
|
_removeFromParentFolderData(path);
|
|
|
|
_notifyWatchers(path, ChangeType.REMOVE);
|
|
}
|
|
|
|
@override
|
|
File getFile(String path) {
|
|
_ensureAbsoluteAndNormalized(path);
|
|
return _MemoryFile(this, path);
|
|
}
|
|
|
|
@override
|
|
Folder getFolder(String path) {
|
|
_ensureAbsoluteAndNormalized(path);
|
|
return _MemoryFolder(this, path);
|
|
}
|
|
|
|
@override
|
|
Link getLink(String path) {
|
|
_ensureAbsoluteAndNormalized(path);
|
|
return _MemoryLink(this, path);
|
|
}
|
|
|
|
@override
|
|
Resource getResource(String path) {
|
|
_ensureAbsoluteAndNormalized(path);
|
|
var data = _pathToData[path];
|
|
return data is _FolderData
|
|
? _MemoryFolder(this, path)
|
|
: _MemoryFile(this, path);
|
|
}
|
|
|
|
@override
|
|
Folder getStateLocation(String pluginId) {
|
|
// ignore: deprecated_member_use_from_same_package
|
|
var path = convertPath('/user/home/$pluginId');
|
|
return newFolder(path);
|
|
}
|
|
|
|
void modifyFile(String path, String content) {
|
|
var data = _pathToData[path];
|
|
if (data is! _FileData) {
|
|
throw FileSystemException(path, 'Not a file.');
|
|
}
|
|
|
|
var bytes = const Utf8Encoder().convert(content);
|
|
_setFileContent(path, bytes);
|
|
}
|
|
|
|
File newFile(String path, String content) {
|
|
var bytes = const Utf8Encoder().convert(content);
|
|
return newFileWithBytes(path, bytes);
|
|
}
|
|
|
|
File newFileWithBytes(String path, List<int> bytes) {
|
|
_ensureAbsoluteAndNormalized(path);
|
|
bytes = bytes is Uint8List ? bytes : Uint8List.fromList(bytes);
|
|
|
|
return _setFileContent(path, bytes);
|
|
}
|
|
|
|
Folder newFolder(String path) {
|
|
_newFolder(path);
|
|
return _MemoryFolder(this, path);
|
|
}
|
|
|
|
/// Create a link from the [path] to the [target].
|
|
void newLink(String path, String target) {
|
|
_ensureAbsoluteAndNormalized(path);
|
|
_ensureAbsoluteAndNormalized(target);
|
|
_pathToLinkedPath[path] = target;
|
|
}
|
|
|
|
/// Write a representation of the file system on the given [sink].
|
|
void writeOn(StringSink sink) {
|
|
List<String> paths = _pathToData.keys.toList();
|
|
paths.sort();
|
|
paths.forEach(sink.writeln);
|
|
}
|
|
|
|
void _addToParentFolderData(_FolderData parentData, String path) {
|
|
var childName = pathContext.basename(path);
|
|
if (!parentData.childNames.contains(childName)) {
|
|
parentData.childNames.add(childName);
|
|
}
|
|
}
|
|
|
|
/// The file system abstraction supports only absolute and normalized paths.
|
|
/// This method is used to validate any input paths to prevent errors later.
|
|
void _ensureAbsoluteAndNormalized(String path) {
|
|
if (!pathContext.isAbsolute(path)) {
|
|
throw ArgumentError("Path must be absolute : $path");
|
|
}
|
|
if (pathContext.normalize(path) != path) {
|
|
throw ArgumentError("Path must be normalized : $path");
|
|
}
|
|
}
|
|
|
|
_FolderData _newFolder(String path) {
|
|
_ensureAbsoluteAndNormalized(path);
|
|
|
|
var data = _pathToData[path];
|
|
if (data is _FolderData) {
|
|
return data;
|
|
} else if (data == null) {
|
|
var parentPath = pathContext.dirname(path);
|
|
if (parentPath != path) {
|
|
var parentData = _newFolder(parentPath);
|
|
_addToParentFolderData(parentData, path);
|
|
}
|
|
var data = _FolderData();
|
|
_pathToData[path] = data;
|
|
_notifyWatchers(path, ChangeType.ADD);
|
|
return data;
|
|
} else {
|
|
throw FileSystemException(path, 'Folder expected.');
|
|
}
|
|
}
|
|
|
|
void _notifyWatchers(String path, ChangeType changeType) {
|
|
_pathToWatchers.forEach((
|
|
String watcherPath,
|
|
List<StreamController<WatchEvent>> streamControllers,
|
|
) {
|
|
if (watcherPath == path || pathContext.isWithin(watcherPath, path)) {
|
|
for (StreamController<WatchEvent> streamController
|
|
in streamControllers) {
|
|
streamController.add(WatchEvent(changeType, path));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void _removeFromParentFolderData(String path) {
|
|
var parentPath = pathContext.dirname(path);
|
|
var parentData = _pathToData[parentPath] as _FolderData;
|
|
var childName = pathContext.basename(path);
|
|
parentData.childNames.remove(childName);
|
|
}
|
|
|
|
void _renameFileSync(String path, String newPath) {
|
|
var data = _pathToData[path];
|
|
if (data is! _FileData) {
|
|
throw FileSystemException(path, 'Not a file.');
|
|
}
|
|
|
|
if (newPath == path) {
|
|
return;
|
|
}
|
|
|
|
var existingNewData = _pathToData[newPath];
|
|
if (existingNewData == null) {
|
|
// Nothing to do.
|
|
} else if (existingNewData is _FileData) {
|
|
deleteFile(newPath);
|
|
} else {
|
|
throw FileSystemException(newPath, 'Not a file.');
|
|
}
|
|
|
|
var parentPath = pathContext.dirname(path);
|
|
var parentData = _newFolder(parentPath);
|
|
_addToParentFolderData(parentData, path);
|
|
|
|
_pathToData.remove(path);
|
|
_pathToData[newPath] = data;
|
|
|
|
_notifyWatchers(path, ChangeType.REMOVE);
|
|
_notifyWatchers(newPath, ChangeType.ADD);
|
|
}
|
|
|
|
String _resolveLinks(String path) {
|
|
var parentPath = _pathContext.dirname(path);
|
|
if (parentPath == path) {
|
|
return path;
|
|
}
|
|
|
|
var canonicalParentPath = _resolveLinks(parentPath);
|
|
|
|
var baseName = _pathContext.basename(path);
|
|
var result = _pathContext.join(canonicalParentPath, baseName);
|
|
|
|
do {
|
|
var linkTarget = _pathToLinkedPath[result];
|
|
if (linkTarget != null) {
|
|
result = linkTarget;
|
|
} else {
|
|
break;
|
|
}
|
|
} while (true);
|
|
|
|
return result;
|
|
}
|
|
|
|
File _setFileContent(String path, Uint8List bytes) {
|
|
var parentPath = pathContext.dirname(path);
|
|
var parentData = _newFolder(parentPath);
|
|
_addToParentFolderData(parentData, path);
|
|
|
|
var exists = _pathToData.containsKey(path);
|
|
_pathToData[path] = _FileData(bytes: bytes, timeStamp: nextStamp++);
|
|
_notifyWatchers(path, exists ? ChangeType.MODIFY : ChangeType.ADD);
|
|
|
|
return _MemoryFile(this, path);
|
|
}
|
|
}
|
|
|
|
class _FileData extends _ResourceData {
|
|
final Uint8List bytes;
|
|
final int timeStamp;
|
|
|
|
_FileData({required this.bytes, required this.timeStamp});
|
|
}
|
|
|
|
class _FolderData extends _ResourceData {
|
|
/// Names (not paths) of direct children.
|
|
final List<String> childNames = [];
|
|
}
|
|
|
|
/// An in-memory implementation of [File].
|
|
class _MemoryFile extends _MemoryResource implements File {
|
|
_MemoryFile(super.provider, super.path);
|
|
|
|
@override
|
|
bool get exists {
|
|
var canonicalPath = provider._resolveLinks(path);
|
|
return provider._pathToData[canonicalPath] is _FileData;
|
|
}
|
|
|
|
@override
|
|
int get lengthSync {
|
|
return readAsBytesSync().length;
|
|
}
|
|
|
|
@override
|
|
int get modificationStamp {
|
|
var canonicalPath = provider._resolveLinks(path);
|
|
var data = provider._pathToData[canonicalPath];
|
|
if (data is! _FileData) {
|
|
throw FileSystemException(path, 'File does not exist.');
|
|
}
|
|
return data.timeStamp;
|
|
}
|
|
|
|
@override
|
|
File copyTo(Folder parentFolder) {
|
|
parentFolder.create();
|
|
File destination = parentFolder.getFile(shortName);
|
|
destination.writeAsBytesSync(readAsBytesSync());
|
|
return destination;
|
|
}
|
|
|
|
@override
|
|
void delete() {
|
|
provider.deleteFile(path);
|
|
}
|
|
|
|
@override
|
|
bool isOrContains(String path) {
|
|
return path == this.path;
|
|
}
|
|
|
|
@override
|
|
Uint8List readAsBytesSync() {
|
|
var canonicalPath = provider._resolveLinks(path);
|
|
var data = provider._pathToData[canonicalPath];
|
|
if (data is! _FileData) {
|
|
throw FileSystemException(path, 'File does not exist.');
|
|
}
|
|
return data.bytes;
|
|
}
|
|
|
|
@override
|
|
String readAsStringSync() {
|
|
var bytes = readAsBytesSync();
|
|
return utf8.decode(bytes);
|
|
}
|
|
|
|
@override
|
|
File renameSync(String newPath) {
|
|
provider._renameFileSync(path, newPath);
|
|
return provider.getFile(newPath);
|
|
}
|
|
|
|
@override
|
|
File resolveSymbolicLinksSync() {
|
|
var canonicalPath = provider._resolveLinks(path);
|
|
var result = provider.getFile(canonicalPath);
|
|
|
|
if (!result.exists) {
|
|
throw FileSystemException(path, 'File does not exist.');
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
@override
|
|
void writeAsBytesSync(List<int> bytes) {
|
|
bytes = bytes is Uint8List ? bytes : Uint8List.fromList(bytes);
|
|
provider._setFileContent(path, bytes);
|
|
}
|
|
|
|
@override
|
|
void writeAsStringSync(String content) {
|
|
var bytes = const Utf8Encoder().convert(content);
|
|
writeAsBytesSync(bytes);
|
|
}
|
|
}
|
|
|
|
/// An in-memory implementation of [Folder].
|
|
class _MemoryFolder extends _MemoryResource implements Folder {
|
|
_MemoryFolder(super.provider, super.path);
|
|
|
|
@override
|
|
bool get exists {
|
|
var canonicalPath = provider._resolveLinks(path);
|
|
return provider._pathToData[canonicalPath] is _FolderData;
|
|
}
|
|
|
|
@override
|
|
bool get isRoot {
|
|
var parentPath = provider.pathContext.dirname(path);
|
|
return parentPath == path;
|
|
}
|
|
|
|
@override
|
|
String canonicalizePath(String relPath) {
|
|
relPath = provider.pathContext.normalize(relPath);
|
|
String childPath = provider.pathContext.join(path, relPath);
|
|
childPath = provider.pathContext.normalize(childPath);
|
|
return childPath;
|
|
}
|
|
|
|
@override
|
|
bool contains(String path) {
|
|
return provider.pathContext.isWithin(this.path, path);
|
|
}
|
|
|
|
@override
|
|
Folder copyTo(Folder parentFolder) {
|
|
Folder destination = parentFolder.getFolder(shortName);
|
|
destination.create();
|
|
for (Resource child in getChildren()) {
|
|
child.copyTo(destination);
|
|
}
|
|
return destination;
|
|
}
|
|
|
|
@override
|
|
void create() {
|
|
provider.newFolder(path);
|
|
}
|
|
|
|
@override
|
|
void delete() {
|
|
provider.deleteFolder(path);
|
|
}
|
|
|
|
@override
|
|
Resource getChild(String relPath) {
|
|
var path = canonicalizePath(relPath);
|
|
return provider.getResource(path);
|
|
}
|
|
|
|
@Deprecated('Use getFile instead.')
|
|
@override
|
|
_MemoryFile getChildAssumingFile(String relPath) {
|
|
return getFile(relPath);
|
|
}
|
|
|
|
@Deprecated('Use getFolder instead.')
|
|
@override
|
|
_MemoryFolder getChildAssumingFolder(String relPath) {
|
|
return getFolder(relPath);
|
|
}
|
|
|
|
@override
|
|
List<Resource> getChildren() {
|
|
var canonicalPath = provider._resolveLinks(path);
|
|
if (canonicalPath != path) {
|
|
var target = provider.getFolder(canonicalPath);
|
|
var canonicalChildren = target.getChildren();
|
|
return canonicalChildren.map((child) {
|
|
var childPath = provider.pathContext.join(path, child.shortName);
|
|
if (child is Folder) {
|
|
return _MemoryFolder(provider, childPath);
|
|
} else {
|
|
return _MemoryFile(provider, childPath);
|
|
}
|
|
}).toList();
|
|
}
|
|
|
|
var data = provider._pathToData[path];
|
|
if (data is! _FolderData) {
|
|
throw FileSystemException(path, 'Folder does not exist.');
|
|
}
|
|
|
|
var children = <Resource>[];
|
|
for (var childName in data.childNames) {
|
|
var childPath = provider.pathContext.join(path, childName);
|
|
var child = provider.getResource(childPath);
|
|
children.add(child);
|
|
}
|
|
|
|
provider._pathToLinkedPath.forEach((resourcePath, targetPath) {
|
|
if (provider.pathContext.dirname(resourcePath) == path) {
|
|
var target = provider.getResource(targetPath);
|
|
if (target is File) {
|
|
children.add(_MemoryFile(provider, resourcePath));
|
|
} else if (target is Folder) {
|
|
children.add(_MemoryFolder(provider, resourcePath));
|
|
}
|
|
}
|
|
});
|
|
|
|
return children;
|
|
}
|
|
|
|
@override
|
|
_MemoryFile getFile(String relPath) {
|
|
var path = canonicalizePath(relPath);
|
|
return _MemoryFile(provider, path);
|
|
}
|
|
|
|
@override
|
|
_MemoryFolder getFolder(String relPath) {
|
|
var path = canonicalizePath(relPath);
|
|
return _MemoryFolder(provider, path);
|
|
}
|
|
|
|
@override
|
|
bool isOrContains(String path) {
|
|
if (path == this.path) {
|
|
return true;
|
|
}
|
|
return contains(path);
|
|
}
|
|
|
|
@override
|
|
Folder resolveSymbolicLinksSync() {
|
|
var canonicalPath = provider._resolveLinks(path);
|
|
var result = provider.getFolder(canonicalPath);
|
|
|
|
if (!result.exists) {
|
|
throw FileSystemException(path, 'Folder does not exist.');
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
@override
|
|
Uri toUri() => provider.pathContext.toUri('$path/');
|
|
}
|
|
|
|
/// An in-memory implementation of [File].
|
|
class _MemoryLink implements Link {
|
|
final MemoryResourceProvider provider;
|
|
final String path;
|
|
|
|
_MemoryLink(this.provider, this.path);
|
|
|
|
@override
|
|
bool get exists {
|
|
return provider._pathToLinkedPath.containsKey(path);
|
|
}
|
|
|
|
@override
|
|
void create(String target) {
|
|
provider.newLink(path, target);
|
|
}
|
|
}
|
|
|
|
/// An in-memory implementation of [Resource].
|
|
abstract class _MemoryResource implements Resource {
|
|
@override
|
|
final MemoryResourceProvider provider;
|
|
|
|
@override
|
|
final String path;
|
|
|
|
_MemoryResource(this.provider, this.path);
|
|
|
|
Stream<WatchEvent> get changes => watch().changes;
|
|
|
|
@override
|
|
int get hashCode => path.hashCode;
|
|
|
|
@override
|
|
Folder get parent {
|
|
String parentPath = provider.pathContext.dirname(path);
|
|
return provider.getFolder(parentPath);
|
|
}
|
|
|
|
@override
|
|
String get shortName => provider.pathContext.basename(path);
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (runtimeType != other.runtimeType) {
|
|
return false;
|
|
}
|
|
return path == (other as _MemoryResource).path;
|
|
}
|
|
|
|
@override
|
|
String toString() => path;
|
|
|
|
@override
|
|
Uri toUri() => provider.pathContext.toUri(path);
|
|
|
|
/// Watch for changes to the files inside this folder (and in any nested
|
|
/// folders, including folders reachable via links).
|
|
///
|
|
/// If `provider.delayWatcherInitialization` is not `null`, this method will
|
|
/// wait for this amount of time before it starts capturing/streaming events
|
|
/// to simulate the delay that occurs when initializing a real file system
|
|
/// watcher.
|
|
@override
|
|
ResourceWatcher watch() {
|
|
var streamController = StreamController<WatchEvent>.broadcast();
|
|
var ready = Completer<void>();
|
|
|
|
/// A helper that sets up the watcher that may be called synchronously
|
|
/// or delayed, depending on the value of
|
|
/// [provider.delayWatcherInitialization].
|
|
void setupWatcher() {
|
|
var watchers = provider._pathToWatchers[path] ??= [];
|
|
watchers.add(streamController);
|
|
unawaited(
|
|
streamController.done.then((_) {
|
|
watchers.remove(streamController);
|
|
if (watchers.isEmpty) {
|
|
provider._pathToWatchers.remove(path);
|
|
}
|
|
}),
|
|
);
|
|
ready.complete();
|
|
if (provider.emitPathNotFoundExceptionsForPaths.contains(path)) {
|
|
streamController.addError(
|
|
PathNotFoundException(
|
|
path,
|
|
'Simulated PathNotFoundException from _MemoryResource',
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
var delayWatcherInitialization = provider.delayWatcherInitialization;
|
|
if (delayWatcherInitialization != null) {
|
|
// Wrap this inside onListen so that (like the real watcher) it will only
|
|
// fire after a listener is attached.
|
|
streamController.onListen = () {
|
|
Future<void>.delayed(delayWatcherInitialization, () {
|
|
streamController.onListen = null;
|
|
setupWatcher();
|
|
});
|
|
};
|
|
} else {
|
|
setupWatcher();
|
|
}
|
|
|
|
return ResourceWatcher(streamController.stream, () => ready.future);
|
|
}
|
|
}
|
|
|
|
class _ResourceData {}
|