file_system.dart 8.12 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' show throwToolExit;
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 33 34 35 36 37 38 39 40
/// Various convenience file system methods.
class FileSystemUtils {
  FileSystemUtils({
    @required FileSystem fileSystem,
    @required Platform platform,
  }) : _fileSystem = fileSystem,
       _platform = platform;

  final FileSystem _fileSystem;

  final Platform _platform;
41

42 43 44 45 46 47 48 49 50 51 52
  /// 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}');
    }
53
  }
54

55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
  /// 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');
92 93
      }
    }
94
  }
95

96 97 98 99 100 101 102 103 104 105 106 107
  /// 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()) {
        return file;
      }
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
      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;
125
    }
126
  }
127

128 129 130 131 132 133
  /// 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;
  }
134

135 136 137 138
  /// Escapes [path].
  ///
  /// On Windows it replaces all '\' with '\\'. On other platforms, it returns the
  /// path unchanged.
139
  String escapePath(String path) => _platform.isWindows ? path.replaceAll(r'\', r'\\') : path;
140

141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
  /// 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);
  }
157

158
  /// Return the absolute path of the user's home directory.
159 160
  String get homeDirPath {
    String path = _platform.isWindows
161 162
      ? _platform.environment['USERPROFILE']
      : _platform.environment['HOME'];
163 164 165 166 167
    if (path != null) {
      path = _fileSystem.path.absolute(path);
    }
    return path;
  }
168
}
169 170 171 172 173 174 175 176 177 178 179 180 181 182

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

183
  // Unless we're in a test of this class's signal handling features, we must
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
  // 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;
  }
}