| // 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:typed_data'; |
| |
| import 'package:file/file.dart'; |
| import 'package:file/src/io.dart' as io; |
| |
| import 'clock.dart'; |
| import 'common.dart'; |
| import 'memory_file_stat.dart'; |
| import 'style.dart'; |
| |
| /// Visitor callback for use with [NodeBasedFileSystem.findNode]. |
| /// |
| /// [parent] is the parent node of the current path segment and is guaranteed |
| /// to be non-null. |
| /// |
| /// [childName] is the basename of the entity at the current path segment. It |
| /// is guaranteed to be non-null. |
| /// |
| /// [childNode] is the node at the current path segment. It will be |
| /// non-null only if such an entity exists. The return value of this callback |
| /// will be used as the value of this node, which allows this callback to |
| /// do things like recursively create or delete folders. |
| /// |
| /// [currentSegment] is the index of the current segment within the overall |
| /// path that's being walked by [NodeBasedFileSystem.findNode]. |
| /// |
| /// [finalSegment] is the index of the final segment that will be walked by |
| /// [NodeBasedFileSystem.findNode]. |
| typedef SegmentVisitor = Node? Function( |
| DirectoryNode parent, |
| String childName, |
| Node? childNode, |
| int currentSegment, |
| int finalSegment, |
| ); |
| |
| /// A [FileSystem] whose internal structure is made up of a tree of [Node] |
| /// instances, rooted at a single node. |
| abstract class NodeBasedFileSystem implements StyleableFileSystem { |
| /// The root node. |
| RootNode? get root; |
| |
| /// The path of the current working directory. |
| String get cwd; |
| |
| /// The clock to use when finding the current time (e.g. to set the creation |
| /// time of a new node). |
| Clock get clock; |
| |
| /// Gets the backing node of the entity at the specified path. If the tail |
| /// element of the path does not exist, this will return null. If the tail |
| /// element cannot be reached because its directory does not exist, a |
| /// [io.FileSystemException] will be thrown. |
| /// |
| /// If [path] is a relative path, it will be resolved relative to |
| /// [reference], or the current working directory ([cwd]) if [reference] is |
| /// null. If [path] is an absolute path, [reference] will be ignored. |
| /// |
| /// If the last element in [path] represents a symbolic link, this will |
| /// return the [LinkNode] node for the link (it will not return the |
| /// node to which the link points), unless [followTailLink] is true. |
| /// Directory links in the _middle_ of the path will be followed in order to |
| /// find the node regardless of the value of [followTailLink]. |
| /// |
| /// If [segmentVisitor] is specified, it will be invoked for every path |
| /// segment visited along the way starting where the reference (root folder |
| /// if the path is absolute) is the parent. For each segment, the return value |
| /// of [segmentVisitor] will be used as the backing node of that path |
| /// segment, thus allowing callers to create nodes on demand in the |
| /// specified path. Note that `..` and `.` segments may cause the visitor to |
| /// get invoked with the same node multiple times. When [segmentVisitor] is |
| /// invoked, for each path segment that resolves to a link node, the visitor |
| /// will visit the actual link node if [visitLinks] is true; otherwise it |
| /// will visit the target of the link node. |
| /// |
| /// If [pathWithSymlinks] is specified, the path to the node with symbolic |
| /// links explicitly broken out will be appended to the buffer. `..` and `.` |
| /// path segments will *not* be resolved and are left to the caller. |
| Node? findNode( |
| String path, { |
| Node reference, |
| SegmentVisitor segmentVisitor, |
| bool visitLinks = false, |
| List<String> pathWithSymlinks, |
| bool followTailLink = false, |
| }); |
| } |
| |
| /// A class that represents the actual storage of an existent file system |
| /// entity (whereas classes [File], [Directory], and [Link] represent less |
| /// concrete entities that may or may not yet exist). |
| /// |
| /// This data structure is loosely based on a Unix-style file system inode |
| /// (hence the name). |
| abstract class Node { |
| /// Constructs a new [Node] as a child of the specified parent. |
| Node(this._parent) { |
| if (_parent == null && !isRoot) { |
| throw const io.FileSystemException('All nodes must have a parent.'); |
| } |
| } |
| |
| DirectoryNode? _parent; |
| |
| /// Gets the directory that holds this node. |
| DirectoryNode get parent => _parent!; |
| |
| /// Reparents this node to live in the specified directory. |
| set parent(DirectoryNode parent) { |
| DirectoryNode ancestor = parent; |
| while (!ancestor.isRoot) { |
| if (ancestor == this) { |
| throw const io.FileSystemException( |
| 'A directory cannot be its own ancestor.'); |
| } |
| ancestor = ancestor.parent; |
| } |
| _parent = parent; |
| } |
| |
| /// Returns the type of the file system entity that this node represents. |
| io.FileSystemEntityType get type; |
| |
| /// Returns the POSIX stat information for this file system object. |
| io.FileStat get stat; |
| |
| /// Returns the closest directory in the ancestry hierarchy starting with |
| /// this node. For directory nodes, it returns the node itself; for other |
| /// nodes, it returns the parent node. |
| DirectoryNode get directory => _parent!; |
| |
| /// Tells if this node is a root node. |
| bool get isRoot => false; |
| |
| /// Returns the file system responsible for this node. |
| NodeBasedFileSystem get fs => _parent!.fs; |
| } |
| |
| /// Base class that represents the backing for those nodes that have |
| /// substance (namely, node types that will not redirect to other types when |
| /// you call [stat] on them). |
| abstract class RealNode extends Node { |
| /// Constructs a new [RealNode] as a child of the specified [parent]. |
| RealNode(DirectoryNode? parent) : super(parent) { |
| int now = clock.now.millisecondsSinceEpoch; |
| changed = now; |
| modified = now; |
| accessed = now; |
| } |
| |
| /// See [NodeBasedFileSystem.clock]. |
| Clock get clock => parent.clock; |
| |
| /// Last changed time in milliseconds since the Epoch. |
| late int changed; |
| |
| /// Last modified time in milliseconds since the Epoch. |
| late int modified; |
| |
| /// Last accessed time in milliseconds since the Epoch. |
| late int accessed; |
| |
| /// Bitmask representing the file read/write/execute mode. |
| int mode = 0x777; |
| |
| @override |
| io.FileStat get stat { |
| return MemoryFileStat( |
| DateTime.fromMillisecondsSinceEpoch(changed), |
| DateTime.fromMillisecondsSinceEpoch(modified), |
| DateTime.fromMillisecondsSinceEpoch(accessed), |
| type, |
| mode, |
| size, |
| ); |
| } |
| |
| /// The size of the file system entity in bytes. |
| int get size; |
| |
| /// Updates the last modified time of the node. |
| void touch() { |
| modified = clock.now.millisecondsSinceEpoch; |
| } |
| } |
| |
| /// Class that represents the backing for an in-memory directory. |
| class DirectoryNode extends RealNode { |
| /// Constructs a new [DirectoryNode] as a child of the specified [parent]. |
| DirectoryNode(DirectoryNode? parent) : super(parent); |
| |
| /// Child nodes, indexed by their basename. |
| final Map<String, Node> children = <String, Node>{}; |
| |
| @override |
| io.FileSystemEntityType get type => io.FileSystemEntityType.directory; |
| |
| @override |
| DirectoryNode get directory => this; |
| |
| @override |
| int get size => 0; |
| } |
| |
| /// Class that represents the backing for the root of the in-memory file system. |
| class RootNode extends DirectoryNode { |
| /// Constructs a new [RootNode] tied to the specified file system. |
| RootNode(this.fs) |
| : assert(fs.root == null), |
| super(null); |
| |
| @override |
| final NodeBasedFileSystem fs; |
| |
| @override |
| Clock get clock => fs.clock; |
| |
| @override |
| DirectoryNode get parent => this; |
| |
| @override |
| bool get isRoot => true; |
| |
| @override |
| set parent(DirectoryNode parent) => |
| throw UnsupportedError('Cannot set the parent of the root directory.'); |
| } |
| |
| /// Class that represents the backing for an in-memory regular file. |
| class FileNode extends RealNode { |
| /// Constructs a new [FileNode] as a child of the specified [parent]. |
| FileNode(DirectoryNode parent) : super(parent); |
| |
| /// File contents in bytes. |
| Uint8List get content => _content; |
| Uint8List _content = Uint8List(0); |
| |
| @override |
| io.FileSystemEntityType get type => io.FileSystemEntityType.file; |
| |
| @override |
| int get size => _content.length; |
| |
| /// Appends the specified bytes to the end of this node's [content]. |
| void write(List<int> bytes) { |
| Uint8List existing = _content; |
| _content = Uint8List(existing.length + bytes.length); |
| _content.setRange(0, existing.length, existing); |
| _content.setRange(existing.length, _content.length, bytes); |
| } |
| |
| /// Truncates this node's [content] to the specified length. |
| /// |
| /// [length] must be in the range \[0, [size]\]. |
| void truncate(int length) { |
| assert(length >= 0); |
| assert(length <= _content.length); |
| _content = _content.sublist(0, length); |
| } |
| |
| /// Clears the [content] of the node. |
| void clear() { |
| _content = Uint8List(0); |
| } |
| |
| /// Copies data from [source] into this node. The [modified] and [changed] |
| /// fields will be reset as opposed to copied to indicate that this file |
| /// has been modified and changed. |
| void copyFrom(FileNode source) { |
| modified = changed = clock.now.millisecondsSinceEpoch; |
| accessed = source.accessed; |
| mode = source.mode; |
| _content = Uint8List.fromList(source.content); |
| } |
| } |
| |
| /// Class that represents the backing for an in-memory symbolic link. |
| class LinkNode extends Node { |
| /// Constructs a new [LinkNode] as a child of the specified [parent] and |
| /// linking to the specified [target] path. |
| LinkNode(DirectoryNode parent, this.target) |
| : assert(target.isNotEmpty), |
| super(parent); |
| |
| /// The path to which this link points. |
| String target; |
| |
| /// A marker used to detect circular link references. |
| bool _reentrant = false; |
| |
| /// Gets the node backing for this link's target. Throws a |
| /// [FileSystemException] if this link references a non-existent file |
| /// system entity. |
| /// |
| /// If [tailVisitor] is specified, it will be invoked for the tail path |
| /// segment of this link's target, and its return value will be used as the |
| /// return value of this method. If the tail path segment of this link's |
| /// target cannot be traversed into, a [FileSystemException] will be thrown, |
| /// and [tailVisitor] will not be invoked. |
| Node getReferent({ |
| Node? tailVisitor(DirectoryNode parent, String childName, Node? child)?, |
| }) { |
| Node? referent = fs.findNode( |
| target, |
| reference: this, |
| segmentVisitor: ( |
| DirectoryNode parent, |
| String childName, |
| Node? child, |
| int currentSegment, |
| int finalSegment, |
| ) { |
| if (tailVisitor != null && currentSegment == finalSegment) { |
| child = tailVisitor(parent, childName, child); |
| } |
| return child; |
| }, |
| ); |
| checkExists(referent, () => target); |
| return referent!; |
| } |
| |
| /// Gets the node backing for this link's target, or null if this link |
| /// references a non-existent file system entity. |
| Node? get referentOrNull { |
| try { |
| return getReferent(); |
| } on io.FileSystemException { |
| return null; |
| } |
| } |
| |
| @override |
| io.FileSystemEntityType get type => io.FileSystemEntityType.link; |
| |
| @override |
| io.FileStat get stat { |
| if (_reentrant) { |
| return MemoryFileStat.notFound; |
| } |
| _reentrant = true; |
| try { |
| Node? node = referentOrNull; |
| return node == null ? MemoryFileStat.notFound : node.stat; |
| } finally { |
| _reentrant = false; |
| } |
| } |
| } |