file_system.dart 8.29 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:file/file.dart';
6
import 'package:file/local.dart' as local_fs;
7
import 'package:meta/meta.dart';
8

9
import 'common.dart';
10
import 'io.dart';
11
import 'platform.dart';
12 13
import 'process.dart';
import 'signals.dart';
14

15 16 17
// 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.
18
export 'package:file/file.dart';
yjbanov's avatar
yjbanov committed
19

20 21 22 23 24 25 26 27
/// 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';
28
}
29

30 31 32
/// Various convenience file system methods.
class FileSystemUtils {
  FileSystemUtils({
33 34
    required FileSystem fileSystem,
    required Platform platform,
35 36 37 38 39 40
  }) : _fileSystem = fileSystem,
       _platform = platform;

  final FileSystem _fileSystem;

  final Platform _platform;
41

42 43 44
  /// Appends a number to a filename in order to make it unique under a
  /// directory.
  File getUniqueFile(Directory dir, String baseName, String ext) {
45
    return _getUniqueFile(dir, baseName, ext);
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
  }

  /// 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;
61
    }
62
  }
63

64 65 66 67
  /// Escapes [path].
  ///
  /// On Windows it replaces all '\' with '\\'. On other platforms, it returns the
  /// path unchanged.
68
  String escapePath(String path) => _platform.isWindows ? path.replaceAll(r'\', r'\\') : path;
69

70 71 72 73 74 75 76
  /// 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({
77 78
    required FileSystemEntity entity,
    required File referenceFile,
79 80 81 82 83 84 85
  }) {
    if (!entity.existsSync()) {
      return true;
    }
    return referenceFile.existsSync()
        && referenceFile.statSync().modified.isAfter(entity.statSync().modified);
  }
86

87
  /// Return the absolute path of the user's home directory.
88 89
  String? get homeDirPath {
    String? path = _platform.isWindows
90 91
      ? _platform.environment['USERPROFILE']
      : _platform.environment['HOME'];
92 93 94 95 96
    if (path != null) {
      path = _fileSystem.path.absolute(path);
    }
    return path;
  }
97
}
98

99 100 101 102 103 104 105
/// Return a relative path if [fullPath] is contained by the cwd, else return an
/// absolute path.
String getDisplayPath(String fullPath, FileSystem fileSystem) {
  final String cwd = fileSystem.currentDirectory.path + fileSystem.path.separator;
  return fullPath.startsWith(cwd) ? fullPath.substring(cwd.length) : fullPath;
}

106 107 108 109 110
/// 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`.
111
/// Does not recurse over directories if [shouldCopyDirectory] returns `false`.
112 113 114
void copyDirectory(
  Directory srcDir,
  Directory destDir, {
115
  bool Function(File srcFile, File destFile)? shouldCopyFile,
116
  bool Function(Directory)? shouldCopyDirectory,
117
  void Function(File srcFile, File destFile)? onFileCopied,
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
}) {
  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 Link) {
      final Link newLink = destDir.fileSystem.link(newPath);
      newLink.createSync(entity.targetSync());
    } else 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) {
140 141 142
      if (shouldCopyDirectory != null && !shouldCopyDirectory(entity)) {
        continue;
      }
143 144 145 146 147 148 149 150 151 152 153 154
      copyDirectory(
        entity,
        destDir.fileSystem.directory(newPath),
        shouldCopyFile: shouldCopyFile,
        onFileCopied: onFileCopied,
      );
    } else {
      throw Exception('${entity.path} is neither File nor Directory, was ${entity.runtimeType}');
    }
  }
}

155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
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(dir.fileSystem.path.join(dir.path, name));
    if (!file.existsSync()) {
      file.createSync(recursive: true);
      return file;
    }
    i += 1;
  }
}

/// Appends a number to a filename in order to make it unique under a
/// directory.
File getUniqueFile(Directory dir, String baseName, String ext) {
  return _getUniqueFile(dir, baseName, ext);
}

176 177 178 179
/// 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 {
180
  LocalFileSystem(this._signals, this._fatalSignals, this._shutdownHooks);
181 182 183

  @visibleForTesting
  LocalFileSystem.test({
184
    required Signals signals,
185
    List<ProcessSignal> fatalSignals = Signals.defaultExitSignals,
186
  }) : this(signals, fatalSignals, null);
187

188
  Directory? _systemTemp;
189
  final Map<ProcessSignal, Object> _signalTokens = <ProcessSignal, Object>{};
190
  final ShutdownHooks? _shutdownHooks;
191

192
  Future<void> dispose() async {
193 194 195 196 197 198 199 200 201 202 203 204 205
    _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) {
206
        _systemTemp?.deleteSync(recursive: true);
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
      }
    } 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) {
222 223 224 225 226 227
      if (!superSystemTempDirectory.existsSync()) {
        throwToolExit('Your system temp directory (${superSystemTempDirectory.path}) does not exist. '
          'Did you set an invalid override in your environment? See issue https://github.com/flutter/flutter/issues/74042 for more context.'
        );
      }
      _systemTemp = superSystemTempDirectory.createTempSync('flutter_tools.')
228
        ..createSync(recursive: true);
229 230 231 232 233 234 235 236 237 238 239 240 241
      // 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.
242
      _shutdownHooks?.addShutdownHook(
243 244 245
        _tryToDeleteTemp,
      );
    }
246
    return _systemTemp!;
247
  }
248 249 250 251

  // This only exist because the memory file system does not support a systemTemp that does not exists #74042
  @visibleForTesting
  Directory get superSystemTempDirectory => super.systemTempDirectory;
252
}