blob: 905ee7a3d2393f0a1bdfbd45ed1c7d346b0f1981 [file] [log] [blame]
// Copyright (c) 2017, 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 'package:file/file.dart';
import 'package:file/src/io.dart' as io;
import 'package:path/path.dart' as p;
import 'common.dart';
import 'memory_directory.dart';
import 'memory_file.dart';
import 'memory_file_stat.dart';
import 'memory_link.dart';
import 'node.dart';
import 'utils.dart' as utils;
const String _thisDir = '.';
const String _parentDir = '..';
/// An implementation of [FileSystem] that exists entirely in memory with an
/// internal representation loosely based on the Filesystem Hierarchy Standard.
/// Notably, this means that this implementation will not look like a Windows
/// file system even if it's being run on a Windows host operating system.
///
/// [MemoryFileSystem] is suitable for mocking and tests, as well as for
/// caching or staging before writing or reading to a live system.
///
/// This implementation of the [FileSystem] interface does not directly use
/// any `dart:io` APIs; it merely uses the library's enum values and interfaces.
/// As such, it is suitable for use in the browser.
abstract class MemoryFileSystem implements FileSystem {
/// Creates a new `MemoryFileSystem`.
///
/// The file system will be empty, and the current directory will be the
/// root directory.
factory MemoryFileSystem() = _MemoryFileSystem;
}
/// Internal implementation of [MemoryFileSystem].
class _MemoryFileSystem extends FileSystem
implements MemoryFileSystem, NodeBasedFileSystem {
RootNode _root;
String _systemTemp;
String _cwd = separator;
/// Creates a new `MemoryFileSystem`.
///
/// The file system will be empty, and the current directory will be the
/// root directory.
_MemoryFileSystem() {
_root = new RootNode(this);
}
@override
RootNode get root => _root;
@override
String get cwd => _cwd;
@override
Directory directory(dynamic path) => new MemoryDirectory(this, getPath(path));
@override
File file(dynamic path) => new MemoryFile(this, getPath(path));
@override
Link link(dynamic path) => new MemoryLink(this, getPath(path));
@override
p.Context get path => new p.Context(style: p.Style.posix, current: _cwd);
/// Gets the system temp directory. This directory will be created on-demand
/// in the root of the file system. Once created, its location is fixed for
/// the life of the process.
@override
Directory get systemTempDirectory {
_systemTemp ??= directory(separator).createTempSync('.tmp_').path;
return directory(_systemTemp)..createSync();
}
@override
Directory get currentDirectory => directory(_cwd);
@override
set currentDirectory(dynamic path) {
String value;
if (path is io.Directory) {
value = path.path;
} else if (path is String) {
value = path;
} else {
throw new ArgumentError('Invalid type for "path": ${path?.runtimeType}');
}
value = directory(value).resolveSymbolicLinksSync();
Node node = findNode(value);
checkExists(node, () => value);
utils.checkIsDir(node, () => value);
assert(utils.isAbsolute(value));
_cwd = value;
}
@override
Future<io.FileStat> stat(String path) async => statSync(path);
@override
io.FileStat statSync(String path) {
try {
return findNode(path)?.stat ?? MemoryFileStat.notFound;
} on io.FileSystemException {
return MemoryFileStat.notFound;
}
}
@override
Future<bool> identical(String path1, String path2) async =>
identicalSync(path1, path2);
@override
bool identicalSync(String path1, String path2) {
Node node1 = findNode(path1);
checkExists(node1, () => path1);
Node node2 = findNode(path2);
checkExists(node2, () => path2);
return node1 != null && node1 == node2;
}
@override
bool get isWatchSupported => false;
@override
Future<io.FileSystemEntityType> type(
String path, {
bool followLinks: true,
}) async =>
typeSync(path, followLinks: followLinks);
@override
io.FileSystemEntityType typeSync(String path, {bool followLinks: true}) {
Node node;
try {
node = findNode(path, followTailLink: followLinks);
} on io.FileSystemException {
node = null;
}
if (node == null) {
return io.FileSystemEntityType.NOT_FOUND;
}
return node.type;
}
/// Gets the node backing for the current working directory. Note that this
/// can return null if the directory has been deleted or moved from under our
/// feet.
DirectoryNode get _current => findNode(_cwd);
@override
Node findNode(
String path, {
Node reference,
SegmentVisitor segmentVisitor,
bool visitLinks: false,
List<String> pathWithSymlinks,
bool followTailLink: false,
}) {
if (path == null) {
throw new ArgumentError.notNull('path');
}
if (utils.isAbsolute(path)) {
reference = _root;
} else {
reference ??= _current;
}
List<String> parts = path.split(separator)..removeWhere(utils.isEmpty);
DirectoryNode directory = reference.directory;
Node child = directory;
int finalSegment = parts.length - 1;
for (int i = 0; i <= finalSegment; i++) {
String basename = parts[i];
assert(basename.isNotEmpty);
switch (basename) {
case _thisDir:
child = directory;
break;
case _parentDir:
child = directory.parent;
directory = directory.parent;
break;
default:
child = directory.children[basename];
}
if (pathWithSymlinks != null) {
pathWithSymlinks.add(basename);
}
PathGenerator subpath = utils.subpath(parts, 0, i);
if (utils.isLink(child) && (i < finalSegment || followTailLink)) {
if (visitLinks || segmentVisitor == null) {
if (segmentVisitor != null) {
child = segmentVisitor(directory, basename, child, i, finalSegment);
}
child = utils.resolveLinks(child, subpath, ledger: pathWithSymlinks);
} else {
child = utils.resolveLinks(
child,
subpath,
ledger: pathWithSymlinks,
tailVisitor: (DirectoryNode parent, String childName, Node child) {
return segmentVisitor(parent, childName, child, i, finalSegment);
},
);
}
} else if (segmentVisitor != null) {
child = segmentVisitor(directory, basename, child, i, finalSegment);
}
if (i < finalSegment) {
checkExists(child, subpath);
utils.checkIsDir(child, subpath);
directory = child;
}
}
return child;
}
}