blob: 6961a74dbe6f438ee7721b2d458e408ec9f8a8b1 [file] [log] [blame]
// 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)';
}