| // 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:meta/meta.dart'; |
| import 'package:path/path.dart' as p; |
| |
| import 'clock.dart'; |
| import 'common.dart'; |
| import 'memory_directory.dart'; |
| import 'memory_file.dart'; |
| import 'memory_file_stat.dart'; |
| import 'memory_link.dart'; |
| import 'node.dart'; |
| import 'style.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. |
| /// |
| /// [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 StyleableFileSystem { |
| /// Creates a new `MemoryFileSystem`. |
| /// |
| /// The file system will be empty, and the current directory will be the |
| /// root directory. |
| /// |
| /// The clock will be a real-time clock; file modification times will |
| /// reflect the real time as reported by the operating system. |
| /// |
| /// If [style] is specified, the file system will use the specified path |
| /// style. The default is [FileSystemStyle.posix]. |
| factory MemoryFileSystem({ |
| FileSystemStyle style = FileSystemStyle.posix, |
| }) => |
| _MemoryFileSystem( |
| style: style, |
| clock: const Clock.realTime(), |
| ); |
| |
| /// Creates a new `MemoryFileSystem` that has a fake clock. |
| /// |
| /// The file system will be empty, and the current directory will be the |
| /// root directory. |
| /// |
| /// The clock will increase monotonically each time it is used, disconnected |
| /// from any real-world clock. |
| /// |
| /// If [style] is specified, the file system will use the specified path |
| /// style. The default is [FileSystemStyle.posix]. |
| factory MemoryFileSystem.test({ |
| FileSystemStyle style = FileSystemStyle.posix, |
| }) => |
| _MemoryFileSystem( |
| style: style, |
| clock: Clock.monotonicTest(), |
| ); |
| } |
| |
| /// Internal implementation of [MemoryFileSystem]. |
| class _MemoryFileSystem extends FileSystem |
| implements MemoryFileSystem, NodeBasedFileSystem { |
| _MemoryFileSystem({ |
| this.style = FileSystemStyle.posix, |
| @required this.clock, |
| }) : assert(style != null), |
| assert(clock != null) { |
| _root = RootNode(this); |
| _context = style.contextFor(style.root); |
| } |
| |
| RootNode _root; |
| String _systemTemp; |
| p.Context _context; |
| |
| @override |
| final Clock clock; |
| |
| @override |
| final FileSystemStyle style; |
| |
| @override |
| RootNode get root => _root; |
| |
| @override |
| String get cwd => _context.current; |
| |
| @override |
| Directory directory(dynamic path) => MemoryDirectory(this, getPath(path)); |
| |
| @override |
| File file(dynamic path) => MemoryFile(this, getPath(path)); |
| |
| @override |
| Link link(dynamic path) => MemoryLink(this, getPath(path)); |
| |
| @override |
| p.Context get path => _context; |
| |
| /// 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(style.root).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 ArgumentError('Invalid type for "path": ${path?.runtimeType}'); |
| } |
| |
| value = directory(value).resolveSymbolicLinksSync(); |
| Node node = findNode(value); |
| checkExists(node, () => value); |
| utils.checkIsDir(node, () => value); |
| assert(_context.isAbsolute(value)); |
| _context = style.contextFor(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.notFound; |
| } |
| 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 ArgumentError.notNull('path'); |
| } |
| |
| if (_context.isAbsolute(path)) { |
| reference = _root; |
| path = path.substring(style.drive.length); |
| } else { |
| reference ??= _current; |
| } |
| |
| List<String> parts = path.split(style.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); |
| } |
| |
| // Generates a subpath for the current segment. |
| String subpath() => parts.sublist(0, i + 1).join(_context.separator); |
| |
| 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; |
| } |
| } |