file_system.dart 7.76 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 'io.dart';
10
import 'platform.dart';
11 12
import 'process.dart';
import 'signals.dart';
13

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

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

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

  final FileSystem _fileSystem;

  final Platform _platform;
40

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

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

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

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

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

98 99 100 101 102 103 104
/// 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;
}

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

154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
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);
}

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

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

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

191
  Future<void> dispose() async {
192 193 194 195 196 197 198 199 200 201 202 203 204
    _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) {
205
        _systemTemp?.deleteSync(recursive: true);
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
      }
    } 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) {
221 222
      _systemTemp = super.systemTempDirectory.createTempSync('flutter_tools.')
        ..createSync(recursive: true);
223 224 225 226 227 228 229 230 231 232 233 234 235
      // 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.
236
      _shutdownHooks?.addShutdownHook(
237 238 239
        _tryToDeleteTemp,
      );
    }
240
    return _systemTemp!;
241 242
  }
}