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