// Copyright 2014 The Flutter Authors. 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.8 import 'package:file/file.dart'; import 'package:file/local.dart' as local_fs; import 'package:meta/meta.dart'; import 'common.dart' show throwToolExit; import 'io.dart'; import 'platform.dart'; import 'process.dart'; import 'signals.dart'; // package:file/local.dart must not be exported. This exposes LocalFileSystem, // which we override to ensure that temporary directories are cleaned up when // the tool is killed by a signal. export 'package:file/file.dart'; /// Exception indicating that a file that was expected to exist was not found. class FileNotFoundException implements IOException { const FileNotFoundException(this.path); final String path; @override String toString() => 'File not found: $path'; } /// Various convenience file system methods. class FileSystemUtils { FileSystemUtils({ @required FileSystem fileSystem, @required Platform platform, }) : _fileSystem = fileSystem, _platform = platform; final FileSystem _fileSystem; final Platform _platform; /// Create the ancestor directories of a file path if they do not already exist. void ensureDirectoryExists(String filePath) { final String dirPath = _fileSystem.path.dirname(filePath); if (_fileSystem.isDirectorySync(dirPath)) { return; } try { _fileSystem.directory(dirPath).createSync(recursive: true); } on FileSystemException catch (e) { throwToolExit('Failed to create directory "$dirPath": ${e.osError.message}'); } } /// Creates `destDir` if needed, then recursively copies `srcDir` to /// `destDir`, invoking [onFileCopied], if specified, for each /// source/destination file pair. /// /// Skips files if [shouldCopyFile] returns `false`. void copyDirectorySync( Directory srcDir, Directory destDir, { bool shouldCopyFile(File srcFile, File destFile), 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 (final 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); if (shouldCopyFile != null && !shouldCopyFile(entity, newFile)) { continue; } newFile.writeAsBytesSync(entity.readAsBytesSync()); onFileCopied?.call(entity, newFile); } else if (entity is Directory) { copyDirectorySync( entity, destDir.fileSystem.directory(newPath), shouldCopyFile: shouldCopyFile, onFileCopied: onFileCopied, ); } else { throw Exception('${entity.path} is neither File nor Directory'); } } } /// Appends a number to a filename in order to make it unique under a /// directory. File getUniqueFile(Directory dir, String baseName, String ext) { final FileSystem fs = dir.fileSystem; int i = 1; while (true) { final String name = '${baseName}_${i.toString().padLeft(2, '0')}.$ext'; final File file = fs.file(_fileSystem.path.join(dir.path, name)); if (!file.existsSync()) { file.createSync(recursive: true); return file; } i += 1; } } /// Appends a number to a directory name in order to make it unique under a /// directory. Directory getUniqueDirectory(Directory dir, String baseName) { final FileSystem fs = dir.fileSystem; int i = 1; while (true) { final String name = '${baseName}_${i.toString().padLeft(2, '0')}'; final Directory directory = fs.directory(_fileSystem.path.join(dir.path, name)); if (!directory.existsSync()) { return directory; } i += 1; } } /// Return a relative path if [fullPath] is contained by the cwd, else return an /// absolute path. String getDisplayPath(String fullPath) { final String cwd = _fileSystem.currentDirectory.path + _fileSystem.path.separator; return fullPath.startsWith(cwd) ? fullPath.substring(cwd.length) : fullPath; } /// Escapes [path]. /// /// On Windows it replaces all '\' with '\\'. On other platforms, it returns the /// path unchanged. String escapePath(String path) => _platform.isWindows ? path.replaceAll(r'\', r'\\') : 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.statSync().modified.isAfter(entity.statSync().modified); } /// Return the absolute path of the user's home directory. String get homeDirPath { String path = _platform.isWindows ? _platform.environment['USERPROFILE'] : _platform.environment['HOME']; if (path != null) { path = _fileSystem.path.absolute(path); } return path; } } /// This class extends [local_fs.LocalFileSystem] in order to clean up /// directories and files that the tool creates under the system temporary /// directory when the tool exits either normally or when killed by a signal. class LocalFileSystem extends local_fs.LocalFileSystem { LocalFileSystem._(Signals signals, List<ProcessSignal> fatalSignals) : _signals = signals, _fatalSignals = fatalSignals; @visibleForTesting LocalFileSystem.test({ @required Signals signals, List<ProcessSignal> fatalSignals = Signals.defaultExitSignals, }) : this._(signals, fatalSignals); // Unless we're in a test of this class's signal handling features, we must // have only one instance created with the singleton LocalSignals instance // and the catchable signals it considers to be fatal. static LocalFileSystem _instance; static LocalFileSystem get instance => _instance ??= LocalFileSystem._( LocalSignals.instance, Signals.defaultExitSignals, ); Directory _systemTemp; final Map<ProcessSignal, Object> _signalTokens = <ProcessSignal, Object>{}; @visibleForTesting static Future<void> dispose() => LocalFileSystem.instance?._dispose(); Future<void> _dispose() async { _tryToDeleteTemp(); for (final MapEntry<ProcessSignal, Object> signalToken in _signalTokens.entries) { await _signals.removeHandler(signalToken.key, signalToken.value); } _signalTokens.clear(); } final Signals _signals; final List<ProcessSignal> _fatalSignals; void _tryToDeleteTemp() { try { if (_systemTemp?.existsSync() ?? false) { _systemTemp.deleteSync(recursive: true); } } on FileSystemException { // ignore. } _systemTemp = null; } // This getter returns a fresh entry under /tmp, like // /tmp/flutter_tools.abcxyz, then the rest of the tool creates /tmp entries // under that, like /tmp/flutter_tools.abcxyz/flutter_build_stuff.123456. // Right before exiting because of a signal or otherwise, we delete // /tmp/flutter_tools.abcxyz, not the whole of /tmp. @override Directory get systemTempDirectory { if (_systemTemp == null) { _systemTemp = super.systemTempDirectory.createTempSync( 'flutter_tools.', )..createSync(recursive: true); // Make sure that the temporary directory is cleaned up if the tool is // killed by a signal. for (final ProcessSignal signal in _fatalSignals) { final Object token = _signals.addHandler( signal, (ProcessSignal _) { _tryToDeleteTemp(); }, ); _signalTokens[signal] = token; } // Make sure that the temporary directory is cleaned up when the tool // exits normally. shutdownHooks?.addShutdownHook( _tryToDeleteTemp, ShutdownStage.CLEANUP, ); } return _systemTemp; } }