blob: a1315cd9217b9c43b8349a91733d4d56204c4e9c [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 'dart:convert';
import 'dart:math' as math show min;
import 'dart:typed_data';
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_file_system_entity.dart';
import 'memory_random_access_file.dart';
import 'node.dart';
import 'utils.dart' as utils;
/// Internal implementation of [File].
class MemoryFile extends MemoryFileSystemEntity implements File {
/// Instantiates a new [MemoryFile].
const MemoryFile(NodeBasedFileSystem fileSystem, String path)
: super(fileSystem, path);
FileNode get _resolvedBackingOrCreate {
Node? node = backingOrNull;
if (node == null) {
node = _doCreate();
} else {
node = utils.isLink(node)
? utils.resolveLinks(node as LinkNode, () => path)
: node;
utils.checkType(expectedType, node.type, () => path);
}
return node as FileNode;
}
@override
io.FileSystemEntityType get expectedType => io.FileSystemEntityType.file;
@override
bool existsSync() => backingOrNull?.stat.type == expectedType;
@override
Future<File> create({bool recursive = false}) async {
createSync(recursive: recursive);
return this;
}
@override
void createSync({bool recursive = false}) {
_doCreate(recursive: recursive);
}
Node? _doCreate({bool recursive = false}) {
Node? node = internalCreateSync(
followTailLink: true,
createChild: (DirectoryNode parent, bool isFinalSegment) {
if (isFinalSegment) {
return FileNode(parent);
} else if (recursive) {
return DirectoryNode(parent);
}
return null;
},
);
if (node?.type != expectedType) {
// There was an existing non-file entity at this object's path
assert(node?.type == FileSystemEntityType.directory);
throw common.isADirectory(path);
}
return node;
}
@override
Future<File> rename(String newPath) async => renameSync(newPath);
@override
File renameSync(String newPath) => internalRenameSync(
newPath,
followTailLink: true,
checkType: (Node node) {
FileSystemEntityType actualType = node.stat.type;
if (actualType != expectedType) {
throw actualType == FileSystemEntityType.notFound
? common.noSuchFileOrDirectory(path)
: common.isADirectory(path);
}
},
) as File;
@override
Future<File> copy(String newPath) async => copySync(newPath);
@override
File copySync(String newPath) {
FileNode sourceNode = resolvedBacking as FileNode;
fileSystem.findNode(
newPath,
segmentVisitor: (
DirectoryNode parent,
String childName,
Node? child,
int currentSegment,
int finalSegment,
) {
if (currentSegment == finalSegment) {
if (child != null) {
if (utils.isLink(child)) {
List<String> ledger = <String>[];
child = utils.resolveLinks(child as LinkNode, () => newPath,
ledger: ledger);
checkExists(child, () => newPath);
parent = child.parent;
childName = ledger.last;
assert(parent.children.containsKey(childName));
}
utils.checkType(expectedType, child.type, () => newPath);
parent.children.remove(childName);
}
FileNode newNode = FileNode(parent);
newNode.copyFrom(sourceNode);
parent.children[childName] = newNode;
}
return child;
},
);
return clone(newPath);
}
@override
Future<int> length() async => lengthSync();
@override
int lengthSync() => (resolvedBacking as FileNode).size;
@override
File get absolute => super.absolute as File;
@override
Future<DateTime> lastAccessed() async => lastAccessedSync();
@override
DateTime lastAccessedSync() => (resolvedBacking as FileNode).stat.accessed;
@override
Future<dynamic> setLastAccessed(DateTime time) async =>
setLastAccessedSync(time);
@override
void setLastAccessedSync(DateTime time) {
FileNode node = resolvedBacking as FileNode;
node.accessed = time.millisecondsSinceEpoch;
}
@override
Future<DateTime> lastModified() async => lastModifiedSync();
@override
DateTime lastModifiedSync() => (resolvedBacking as FileNode).stat.modified;
@override
Future<dynamic> setLastModified(DateTime time) async =>
setLastModifiedSync(time);
@override
void setLastModifiedSync(DateTime time) {
FileNode node = resolvedBacking as FileNode;
node.modified = time.millisecondsSinceEpoch;
}
@override
Future<io.RandomAccessFile> open(
{io.FileMode mode = io.FileMode.read}) async =>
openSync(mode: mode);
@override
io.RandomAccessFile openSync({io.FileMode mode = io.FileMode.read}) {
if (utils.isWriteMode(mode) && !existsSync()) {
// [resolvedBacking] requires that the file already exists, so we must
// create it here first.
createSync();
}
return MemoryRandomAccessFile(path, resolvedBacking as FileNode, mode);
}
@override
Stream<Uint8List> openRead([int? start, int? end]) {
try {
FileNode node = resolvedBacking as FileNode;
Uint8List content = node.content;
if (start != null) {
content = end == null
? content.sublist(start)
: content.sublist(start, math.min(end, content.length));
}
return Stream<Uint8List>.fromIterable(<Uint8List>[content]);
} catch (e) {
return Stream<Uint8List>.fromFuture(Future<Uint8List>.error(e));
}
}
@override
io.IOSink openWrite({
io.FileMode mode = io.FileMode.write,
Encoding encoding = utf8,
}) {
if (!utils.isWriteMode(mode)) {
throw ArgumentError.value(mode, 'mode',
'Must be either WRITE, APPEND, WRITE_ONLY, or WRITE_ONLY_APPEND');
}
return _FileSink.fromFile(this, mode, encoding);
}
@override
Future<Uint8List> readAsBytes() async => readAsBytesSync();
@override
Uint8List readAsBytesSync() =>
Uint8List.fromList((resolvedBacking as FileNode).content);
@override
Future<String> readAsString({Encoding encoding = utf8}) async =>
readAsStringSync(encoding: encoding);
@override
String readAsStringSync({Encoding encoding = utf8}) =>
encoding.decode(readAsBytesSync());
@override
Future<List<String>> readAsLines({Encoding encoding = utf8}) async =>
readAsLinesSync(encoding: encoding);
@override
List<String> readAsLinesSync({Encoding encoding = utf8}) {
String str = readAsStringSync(encoding: encoding);
if (str.isEmpty) {
return <String>[];
}
final List<String> lines = str.split('\n');
if (str.endsWith('\n')) {
// A final newline should not create an additional line.
lines.removeLast();
}
return lines;
}
@override
Future<File> writeAsBytes(
List<int> bytes, {
io.FileMode mode = io.FileMode.write,
bool flush = false,
}) async {
writeAsBytesSync(bytes, mode: mode, flush: flush);
return this;
}
@override
void writeAsBytesSync(
List<int> bytes, {
io.FileMode mode = io.FileMode.write,
bool flush = false,
}) {
if (!utils.isWriteMode(mode)) {
throw common.badFileDescriptor(path);
}
FileNode node = _resolvedBackingOrCreate;
_truncateIfNecessary(node, mode);
node.write(bytes);
node.touch();
}
@override
Future<File> writeAsString(
String contents, {
io.FileMode mode = io.FileMode.write,
Encoding encoding = utf8,
bool flush = false,
}) async {
writeAsStringSync(contents, mode: mode, encoding: encoding, flush: flush);
return this;
}
@override
void writeAsStringSync(
String contents, {
io.FileMode mode = io.FileMode.write,
Encoding encoding = utf8,
bool flush = false,
}) =>
writeAsBytesSync(encoding.encode(contents), mode: mode, flush: flush);
@override
@protected
File clone(String path) => MemoryFile(fileSystem, path);
void _truncateIfNecessary(FileNode node, io.FileMode mode) {
if (mode == io.FileMode.write || mode == io.FileMode.writeOnly) {
node.clear();
}
}
@override
String toString() => "MemoryFile: '$path'";
}
/// Implementation of an [io.IOSink] that's backed by a [FileNode].
class _FileSink implements io.IOSink {
factory _FileSink.fromFile(
MemoryFile file,
io.FileMode mode,
Encoding encoding,
) {
late FileNode node;
Exception? deferredException;
// Resolve the backing immediately to ensure that the [FileNode] we write
// to is the same as when [openWrite] was called. This can matter if the
// file is moved or removed while open.
try {
node = file._resolvedBackingOrCreate;
} on Exception catch (e) {
// For behavioral consistency with [LocalFile], do not report failures
// immediately.
deferredException = e;
}
Future<FileNode> future = Future<FileNode>.microtask(() {
if (deferredException != null) {
throw deferredException;
}
file._truncateIfNecessary(node, mode);
return node;
});
return _FileSink._(future, encoding);
}
_FileSink._(Future<FileNode> _node, this.encoding) : _pendingWrites = _node;
final Completer<void> _completer = Completer<void>();
Future<FileNode> _pendingWrites;
Completer<void>? _streamCompleter;
bool _isClosed = false;
@override
Encoding encoding;
bool get isStreaming => !(_streamCompleter?.isCompleted ?? true);
@override
void add(List<int> data) {
_checkNotStreaming();
if (_isClosed) {
throw StateError('StreamSink is closed');
}
_addData(data);
}
@override
void write(Object? obj) => add(encoding.encode(obj?.toString() ?? 'null'));
@override
void writeAll(Iterable<dynamic> objects, [String separator = '']) {
bool firstIter = true;
for (dynamic obj in objects) {
if (!firstIter) {
write(separator);
}
firstIter = false;
write(obj);
}
}
@override
void writeln([Object? obj = '']) {
write(obj);
write('\n');
}
@override
void writeCharCode(int charCode) => write(String.fromCharCode(charCode));
@override
void addError(Object error, [StackTrace? stackTrace]) {
_checkNotStreaming();
_completer.completeError(error, stackTrace);
}
@override
Future<void> addStream(Stream<List<int>> stream) {
_checkNotStreaming();
_streamCompleter = Completer<void>();
void finish() {
_streamCompleter!.complete();
_streamCompleter = null;
}
stream.listen(
(List<int> data) => _addData(data),
cancelOnError: true,
onError: (Object error, StackTrace stackTrace) {
_completer.completeError(error, stackTrace);
finish();
},
onDone: finish,
);
return _streamCompleter!.future;
}
@override
Future<void> flush() {
_checkNotStreaming();
return _pendingWrites;
}
@override
Future<void> close() {
_checkNotStreaming();
if (!_isClosed) {
_isClosed = true;
_pendingWrites.then(
(_) => _completer.complete(),
onError: (Object error, StackTrace stackTrace) =>
_completer.completeError(error, stackTrace),
);
}
return _completer.future;
}
@override
Future<void> get done => _completer.future;
void _addData(List<int> data) {
_pendingWrites = _pendingWrites.then((FileNode node) {
node.write(data);
return node;
});
}
void _checkNotStreaming() {
if (isStreaming) {
throw StateError('StreamSink is bound to a stream');
}
}
}