| // 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); | |
| } |