blob: fa0b0a6ca9ae187a6c45ac2adad28779bbd685bd [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.
// @dart=2.10
import 'dart:async';
import 'package:file/file.dart';
import 'package:file/src/common.dart' as common;
import 'package:file/src/io.dart' as io;
import 'package:meta/meta.dart';
import 'common.dart';
import 'memory_directory.dart';
import 'node.dart';
import 'style.dart';
import 'utils.dart' as utils;
/// Validator function for use with `_renameSync`. This will be invoked if the
/// rename would overwrite an existing entity at the new path. If this operation
/// should not be allowed, this function is expected to throw a
/// [io.FileSystemException]. The lack of such an exception will be interpreted
/// as the overwrite being permissible.
typedef RenameOverwriteValidator<T extends Node> = void Function(
T existingNode);
/// Base class for all in-memory file system entity types.
abstract class MemoryFileSystemEntity implements FileSystemEntity {
/// Constructor for subclasses.
const MemoryFileSystemEntity(this.fileSystem, this.path);
@override
final NodeBasedFileSystem fileSystem;
@override
final String path;
@override
String get dirname => fileSystem.path.dirname(path);
@override
String get basename => fileSystem.path.basename(path);
/// Returns the expected type of this entity, which may differ from the type
/// of the node that's found at the path specified by this entity.
io.FileSystemEntityType get expectedType;
/// Gets the node that backs this file system entity, or null if this
/// entity does not exist.
@protected
Node? get backingOrNull {
try {
return fileSystem.findNode(path);
} on io.FileSystemException {
return null;
}
}
/// Gets the node that backs this file system entity. Throws a
/// [io.FileSystemException] if this entity doesn't exist.
///
/// The type of the node is not guaranteed to match [expectedType].
@protected
Node get backing {
Node? node = fileSystem.findNode(path);
checkExists(node, () => path);
return node!;
}
/// Gets the node that backs this file system entity, or if that node is
/// a symbolic link, the target node. This also will check that the type of
/// the node (after symlink resolution) matches [expectedType]. If the type
/// doesn't match, this will throw a [io.FileSystemException].
@protected
Node get resolvedBacking {
Node node = backing;
node = utils.isLink(node)
? utils.resolveLinks(node as LinkNode, () => path)
: node;
utils.checkType(expectedType, node.type, () => path);
return node;
}
/// Checks the expected type of this file system entity against the specified
/// node's `stat` type, throwing a [FileSystemException] if the types don't
/// match. Note that since this checks the node's `stat` type, symbolic links
/// will be resolved to their target type for the purpose of this validation.
///
/// Protected methods that accept a `checkType` argument will default to this
/// method if the `checkType` argument is unspecified.
@protected
void defaultCheckType(Node node) {
utils.checkType(expectedType, node.stat.type, () => path);
}
@override
Uri get uri {
return Uri.file(path, windows: fileSystem.style == FileSystemStyle.windows);
}
@override
Future<bool> exists() async => existsSync();
@override
Future<String> resolveSymbolicLinks() async => resolveSymbolicLinksSync();
@override
String resolveSymbolicLinksSync() {
if (path.isEmpty) {
throw common.noSuchFileOrDirectory(path);
}
List<String> ledger = <String>[];
if (isAbsolute) {
ledger.add(fileSystem.style.drive);
}
Node? node = fileSystem.findNode(path,
pathWithSymlinks: ledger, followTailLink: true);
checkExists(node, () => path);
String resolved = ledger.join(fileSystem.path.separator);
if (resolved == fileSystem.style.drive) {
resolved = fileSystem.style.root;
} else if (!fileSystem.path.isAbsolute(resolved)) {
resolved = fileSystem.cwd + fileSystem.path.separator + resolved;
}
return fileSystem.path.normalize(resolved);
}
@override
Future<io.FileStat> stat() => fileSystem.stat(path);
@override
io.FileStat statSync() => fileSystem.statSync(path);
@override
Future<FileSystemEntity> delete({bool recursive = false}) async {
deleteSync(recursive: recursive);
return this;
}
@override
void deleteSync({bool recursive = false}) =>
internalDeleteSync(recursive: recursive);
@override
Stream<io.FileSystemEvent> watch({
int events = io.FileSystemEvent.all,
bool recursive = false,
}) =>
throw UnsupportedError('Watching not supported in MemoryFileSystem');
@override
bool get isAbsolute => fileSystem.path.isAbsolute(path);
@override
FileSystemEntity get absolute {
String absolutePath = path;
if (!fileSystem.path.isAbsolute(absolutePath)) {
absolutePath = fileSystem.path.join(fileSystem.cwd, absolutePath);
}
return clone(absolutePath);
}
@override
Directory get parent => MemoryDirectory(fileSystem, dirname);
/// Helper method for subclasses wishing to synchronously create this entity.
/// This method will traverse the path to this entity one segment at a time,
/// calling [createChild] for each segment whose child does not already exist.
///
/// When [createChild] is invoked:
/// - `parent` will be the parent node for the current segment and is
/// guaranteed to be non-null.
/// - `isFinalSegment` will indicate whether the current segment is the tail
/// segment, which in turn indicates that this is the segment into which to
/// create the node for this entity.
///
/// This method returns with the backing node for the entity at this [path].
/// If an entity already existed at this path, [createChild] will not be
/// invoked at all, and this method will return with the backing node for the
/// existing entity (whose type may differ from this entity's type).
///
/// If [followTailLink] is true and the result node is a link, this will
/// resolve it to its target prior to returning it.
@protected
Node? internalCreateSync({
required Node? createChild(DirectoryNode parent, bool isFinalSegment),
bool followTailLink = false,
bool visitLinks = false,
}) {
return fileSystem.findNode(
path,
followTailLink: followTailLink,
visitLinks: visitLinks,
segmentVisitor: (
DirectoryNode parent,
String childName,
Node? child,
int currentSegment,
int finalSegment,
) {
if (child == null) {
assert(!parent.children.containsKey(childName));
child = createChild(parent, currentSegment == finalSegment);
if (child != null) {
parent.children[childName] = child;
}
}
return child;
},
);
}
/// Helper method for subclasses wishing to synchronously rename this entity.
/// This method will look for an existing file system entity at the location
/// identified by [newPath], and if it finds an existing entity, it will check
/// the following:
///
/// - If the entity is of a different type than this entity, the operation
/// will fail, and a [io.FileSystemException] will be thrown.
/// - If the caller has specified [validateOverwriteExistingEntity], then that
/// method will be invoked and passed the node backing of the existing
/// entity that would overwritten by the rename action. That callback is
/// expected to throw a [io.FileSystemException] if overwriting the existing
/// entity is not allowed.
///
/// If the previous two checks pass, or if there was no existing entity at
/// the specified location, this will perform the rename.
///
/// If [newPath] cannot be traversed to because its directory does not exist,
/// a [io.FileSystemException] will be thrown.
///
/// If [followTailLink] is true and there is an existing link at the location
/// identified by [newPath], this will resolve the link to its target prior
/// to running the validation checks above.
///
/// If [checkType] is specified, it will be used to validate that the file
/// system entity that exists at [path] is of the expected type. By default,
/// [defaultCheckType] is used to perform this validation.
@protected
FileSystemEntity internalRenameSync<T extends Node>(
String newPath, {
RenameOverwriteValidator<T>? validateOverwriteExistingEntity,
bool followTailLink = false,
utils.TypeChecker? checkType,
}) {
Node node = backing;
(checkType ?? defaultCheckType)(node);
fileSystem.findNode(
newPath,
segmentVisitor: (
DirectoryNode parent,
String childName,
Node? child,
int currentSegment,
int finalSegment,
) {
if (currentSegment == finalSegment) {
if (child != null) {
if (followTailLink) {
FileSystemEntityType childType = child.stat.type;
if (childType != FileSystemEntityType.notFound) {
utils.checkType(expectedType, child.stat.type, () => newPath);
}
} else {
utils.checkType(expectedType, child.type, () => newPath);
}
if (validateOverwriteExistingEntity != null) {
validateOverwriteExistingEntity(child as T);
}
parent.children.remove(childName);
}
node.parent.children.remove(basename);
parent.children[childName] = node;
node.parent = parent;
}
return child;
},
);
return clone(newPath);
}
/// Deletes this entity from the node tree.
///
/// If [checkType] is specified, it will be used to validate that the file
/// system entity that exists at [path] is of the expected type. By default,
/// [defaultCheckType] is used to perform this validation.
@protected
void internalDeleteSync({
bool recursive = false,
utils.TypeChecker? checkType,
}) {
Node node = backing;
if (!recursive) {
if (node is DirectoryNode && node.children.isNotEmpty) {
throw common.directoryNotEmpty(path);
}
(checkType ?? defaultCheckType)(node);
}
// Once we remove this reference, the node and all its children will be
// garbage collected; we don't need to explicitly delete all children in
// the recursive:true case.
node.parent.children.remove(basename);
}
/// Creates a new entity with the same type as this entity but with the
/// specified path.
@protected
FileSystemEntity clone(String path);
}