1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:file/memory.dart';
import 'package:file/record_replay.dart';
import 'package:meta/meta.dart';
import 'common.dart' show throwToolExit;
import 'context.dart';
import 'platform.dart';
import 'process.dart';
export 'package:file/file.dart';
export 'package:file/local.dart';
const String _kRecordingType = 'file';
const FileSystem _kLocalFs = LocalFileSystem();
/// Currently active implementation of the file system.
///
/// By default it uses local disk-based implementation. Override this in tests
/// with [MemoryFileSystem].
FileSystem get fs => context[FileSystem] ?? _kLocalFs;
/// Gets a [FileSystem] that will record file system activity to the specified
/// base recording [location].
///
/// Activity will be recorded in a subdirectory of [location] named `"file"`.
/// It is permissible for [location] to represent an existing non-empty
/// directory as long as there is no collision with the `"file"` subdirectory.
RecordingFileSystem getRecordingFileSystem(String location) {
final Directory dir = getRecordingSink(location, _kRecordingType);
final RecordingFileSystem fileSystem = RecordingFileSystem(
delegate: _kLocalFs, destination: dir);
addShutdownHook(() async {
await fileSystem.recording.flush(
pendingResultTimeout: const Duration(seconds: 5),
);
}, ShutdownStage.SERIALIZE_RECORDING);
return fileSystem;
}
/// Gets a [FileSystem] that replays invocation activity from a previously
/// recorded set of invocations.
///
/// [location] must represent a directory to which file system activity has
/// been recorded (i.e. the result of having been previously passed to
/// [getRecordingFileSystem]), or a [ToolExit] will be thrown.
ReplayFileSystem getReplayFileSystem(String location) {
final Directory dir = getReplaySource(location, _kRecordingType);
return ReplayFileSystem(recording: dir);
}
/// Create the ancestor directories of a file path if they do not already exist.
void ensureDirectoryExists(String filePath) {
final String dirPath = fs.path.dirname(filePath);
if (fs.isDirectorySync(dirPath))
return;
try {
fs.directory(dirPath).createSync(recursive: true);
} on FileSystemException catch (e) {
throwToolExit('Failed to create directory "$dirPath": ${e.osError.message}');
}
}
/// Recursively copies `srcDir` to `destDir`, invoking [onFileCopied] if
/// specified for each source/destination file pair.
///
/// Creates `destDir` if needed.
void copyDirectorySync(Directory srcDir, Directory destDir, [void onFileCopied(File srcFile, File destFile)]) {
if (!srcDir.existsSync())
throw Exception('Source directory "${srcDir.path}" does not exist, nothing to copy');
if (!destDir.existsSync())
destDir.createSync(recursive: true);
for (FileSystemEntity entity in srcDir.listSync()) {
final String newPath = destDir.fileSystem.path.join(destDir.path, entity.basename);
if (entity is File) {
final File newFile = destDir.fileSystem.file(newPath);
newFile.writeAsBytesSync(entity.readAsBytesSync());
onFileCopied?.call(entity, newFile);
} else if (entity is Directory) {
copyDirectorySync(
entity, destDir.fileSystem.directory(newPath));
} else {
throw Exception('${entity.path} is neither File nor Directory');
}
}
}
/// Gets a directory to act as a recording destination, creating the directory
/// as necessary.
///
/// The directory will exist in the local file system, be named [basename], and
/// be a child of the directory identified by [dirname].
///
/// If the target directory already exists as a directory, the existing
/// directory must be empty, or a [ToolExit] will be thrown. If the target
/// directory exists as an entity other than a directory, a [ToolExit] will
/// also be thrown.
Directory getRecordingSink(String dirname, String basename) {
final String location = _kLocalFs.path.join(dirname, basename);
switch (_kLocalFs.typeSync(location, followLinks: false)) {
case FileSystemEntityType.file:
case FileSystemEntityType.link:
throwToolExit('Invalid record-to location: $dirname ("$basename" exists as non-directory)');
break;
case FileSystemEntityType.directory:
if (_kLocalFs.directory(location).listSync(followLinks: false).isNotEmpty)
throwToolExit('Invalid record-to location: $dirname ("$basename" is not empty)');
break;
case FileSystemEntityType.notFound:
_kLocalFs.directory(location).createSync(recursive: true);
}
return _kLocalFs.directory(location);
}
/// Gets a directory that holds a saved recording to be used for the purpose of
/// replay.
///
/// The directory will exist in the local file system, be named [basename], and
/// be a child of the directory identified by [dirname].
///
/// If the target directory does not exist, a [ToolExit] will be thrown.
Directory getReplaySource(String dirname, String basename) {
final Directory dir = _kLocalFs.directory(_kLocalFs.path.join(dirname, basename));
if (!dir.existsSync())
throwToolExit('Invalid replay-from location: $dirname ("$basename" does not exist)');
return dir;
}
/// Canonicalizes [path].
///
/// This function implements the behavior of `canonicalize` from
/// `package:path`. However, unlike the original, it does not change the ASCII
/// case of the path. Changing the case can break hot reload in some situations,
/// for an example see: https://github.com/flutter/flutter/issues/9539.
String canonicalizePath(String path) => fs.path.normalize(fs.path.absolute(path));
/// Escapes [path].
///
/// On Windows it replaces all '\' with '\\'. On other platforms, it returns the
/// path unchanged.
String escapePath(String path) => platform.isWindows ? path.replaceAll('\\', '\\\\') : path;
/// Returns true if the file system [entity] has not been modified since the
/// latest modification to [referenceFile].
///
/// Returns true, if [entity] does not exist.
///
/// Returns false, if [entity] exists, but [referenceFile] does not.
bool isOlderThanReference({@required FileSystemEntity entity, @required File referenceFile}) {
if (!entity.existsSync())
return true;
return referenceFile.existsSync()
&& referenceFile.lastModifiedSync().isAfter(entity.statSync().modified);
}