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