| // Copyright 2014 The Flutter Authors. 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:io' as io |
| show |
| Directory, |
| File, |
| FileStat, |
| FileSystemEntity, |
| FileSystemEntityType, |
| Link; |
| |
| import 'package:file/file.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:path/path.dart' as p; // flutter_ignore: package_path_import |
| |
| /// A [FileSystem] that wraps the [delegate] file system to create an overlay of |
| /// files from multiple [roots]. |
| /// |
| /// Regular paths or `file:` URIs are resolved directly in the underlying file |
| /// system, but URIs that use a special [scheme] are resolved by searching |
| /// under a set of given roots in order. |
| /// |
| /// For example, consider the following inputs: |
| /// |
| /// - scheme is `multi-root` |
| /// - the set of roots are `/a` and `/b` |
| /// - the underlying file system contains files: |
| /// /root_a/dir/only_a.dart |
| /// /root_a/dir/both.dart |
| /// /root_b/dir/only_b.dart |
| /// /root_b/dir/both.dart |
| /// /other/other.dart |
| /// |
| /// Then: |
| /// |
| /// - file:///other/other.dart is resolved as /other/other.dart |
| /// - multi-root:///dir/only_a.dart is resolved as /root_a/dir/only_a.dart |
| /// - multi-root:///dir/only_b.dart is resolved as /root_b/dir/only_b.dart |
| /// - multi-root:///dir/both.dart is resolved as /root_a/dir/only_a.dart |
| class MultiRootFileSystem extends ForwardingFileSystem { |
| MultiRootFileSystem({ |
| required FileSystem delegate, |
| required String scheme, |
| required List<String> roots, |
| }) : assert(delegate != null), |
| assert(roots.isNotEmpty), |
| _scheme = scheme, |
| _roots = roots.map((String root) => delegate.path.normalize(root)).toList(), |
| super(delegate); |
| |
| @visibleForTesting |
| FileSystem get fileSystem => delegate; |
| |
| final String _scheme; |
| final List<String> _roots; |
| |
| @override |
| File file(dynamic path) => MultiRootFile( |
| fileSystem: this, |
| delegate: delegate.file(_resolve(path)), |
| ); |
| |
| @override |
| Directory directory(dynamic path) => MultiRootDirectory( |
| fileSystem: this, |
| delegate: delegate.directory(_resolve(path)), |
| ); |
| |
| @override |
| Link link(dynamic path) => MultiRootLink( |
| fileSystem: this, |
| delegate: delegate.link(_resolve(path)), |
| ); |
| |
| @override |
| Future<io.FileStat> stat(String path) => |
| delegate.stat(_resolve(path).toString()); |
| |
| @override |
| io.FileStat statSync(String path) => |
| delegate.statSync(_resolve(path).toString()); |
| |
| @override |
| Future<bool> identical(String path1, String path2) => |
| delegate.identical(_resolve(path1).toString(), _resolve(path2).toString()); |
| |
| @override |
| bool identicalSync(String path1, String path2) => |
| delegate.identicalSync(_resolve(path1).toString(), _resolve(path2).toString()); |
| |
| @override |
| Future<io.FileSystemEntityType> type(String path, {bool followLinks = true}) => |
| delegate.type(_resolve(path).toString(), followLinks: followLinks); |
| |
| @override |
| io.FileSystemEntityType typeSync(String path, {bool followLinks = true}) => |
| delegate.typeSync(_resolve(path).toString(), followLinks: followLinks); |
| |
| // Caching the path context here and clearing when the currentDirectory setter |
| // is updated works since the flutter tool restricts usage of dart:io directly |
| // via the forbidden import tests. Otherwise, the path context's current |
| // working directory might get out of sync, leading to unexpected results from |
| // methods like `path.relative`. |
| @override |
| p.Context get path => _cachedPath ??= delegate.path; |
| p.Context? _cachedPath; |
| |
| @override |
| set currentDirectory(dynamic path) { |
| _cachedPath = null; |
| delegate.currentDirectory = path; |
| } |
| |
| /// If the path is a multiroot uri, resolve to the actual path of the |
| /// underlying file system. Otherwise, return as is. |
| dynamic _resolve(dynamic path) { |
| Uri uri; |
| if (path == null) { |
| return null; |
| } else if (path is String) { |
| uri = Uri.parse(path); |
| } else if (path is Uri) { |
| uri = path; |
| } else if (path is FileSystemEntity) { |
| uri = path.uri; |
| } else { |
| throw ArgumentError('Invalid type for "path": ${(path as Object?)?.runtimeType}'); |
| } |
| |
| if (!uri.hasScheme || uri.scheme != _scheme) { |
| return path; |
| } |
| |
| String? firstRootPath; |
| final String relativePath = delegate.path.joinAll(uri.pathSegments); |
| for (final String root in _roots) { |
| final String pathWithRoot = delegate.path.join(root, relativePath); |
| if (delegate.typeSync(pathWithRoot, followLinks: false) != |
| FileSystemEntityType.notFound) { |
| return pathWithRoot; |
| } |
| firstRootPath ??= pathWithRoot; |
| } |
| |
| // If not found, construct the path with the first root. |
| return firstRootPath!; |
| } |
| |
| Uri _toMultiRootUri(Uri uri) { |
| if (uri.scheme != 'file') { |
| return uri; |
| } |
| |
| final p.Context pathContext = delegate.path; |
| final bool isWindows = pathContext.style == p.Style.windows; |
| final String path = uri.toFilePath(windows: isWindows); |
| for (final String root in _roots) { |
| if (path.startsWith('$root${pathContext.separator}')) { |
| String pathWithoutRoot = path.substring(root.length + 1); |
| if (isWindows) { |
| // Convert the path from Windows style |
| pathWithoutRoot = p.url.joinAll(pathContext.split(pathWithoutRoot)); |
| } |
| return Uri.parse('$_scheme:///$pathWithoutRoot'); |
| } |
| } |
| return uri; |
| } |
| |
| @override |
| String toString() => |
| 'MultiRootFileSystem(scheme = $_scheme, roots = $_roots, delegate = $delegate)'; |
| } |
| |
| abstract class MultiRootFileSystemEntity<T extends FileSystemEntity, |
| D extends io.FileSystemEntity> extends ForwardingFileSystemEntity<T, D> { |
| MultiRootFileSystemEntity({ |
| required this.fileSystem, |
| required this.delegate, |
| }); |
| |
| @override |
| final D delegate; |
| |
| @override |
| final MultiRootFileSystem fileSystem; |
| |
| @override |
| File wrapFile(io.File delegate) => MultiRootFile( |
| fileSystem: fileSystem, |
| delegate: delegate, |
| ); |
| |
| @override |
| Directory wrapDirectory(io.Directory delegate) => MultiRootDirectory( |
| fileSystem: fileSystem, |
| delegate: delegate, |
| ); |
| |
| @override |
| Link wrapLink(io.Link delegate) => MultiRootLink( |
| fileSystem: fileSystem, |
| delegate: delegate, |
| ); |
| |
| @override |
| Uri get uri => fileSystem._toMultiRootUri(delegate.uri); |
| } |
| |
| class MultiRootFile extends MultiRootFileSystemEntity<File, io.File> |
| with ForwardingFile { |
| MultiRootFile({ |
| required super.fileSystem, |
| required super.delegate, |
| }); |
| |
| @override |
| String toString() => |
| 'MultiRootFile(fileSystem = $fileSystem, delegate = $delegate)'; |
| } |
| |
| class MultiRootDirectory |
| extends MultiRootFileSystemEntity<Directory, io.Directory> |
| with ForwardingDirectory<Directory> { |
| MultiRootDirectory({ |
| required super.fileSystem, |
| required super.delegate, |
| }); |
| |
| // For the childEntity methods, we first obtain an instance of the entity |
| // from the underlying file system, then invoke childEntity() on it, then |
| // wrap in the ErrorHandling version. |
| @override |
| Directory childDirectory(String basename) => |
| fileSystem.directory(fileSystem.path.join(delegate.path, basename)); |
| |
| @override |
| File childFile(String basename) => |
| fileSystem.file(fileSystem.path.join(delegate.path, basename)); |
| |
| @override |
| Link childLink(String basename) => |
| fileSystem.link(fileSystem.path.join(delegate.path, basename)); |
| |
| @override |
| String toString() => |
| 'MultiRootDirectory(fileSystem = $fileSystem, delegate = $delegate)'; |
| } |
| |
| class MultiRootLink extends MultiRootFileSystemEntity<Link, io.Link> |
| with ForwardingLink { |
| MultiRootLink({ |
| required super.fileSystem, |
| required super.delegate, |
| }); |
| |
| @override |
| String toString() => |
| 'MultiRootLink(fileSystem = $fileSystem, delegate = $delegate)'; |
| } |