file_system.dart 6.29 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6 7
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:file/memory.dart';
8
import 'package:file/record_replay.dart';
9
import 'package:meta/meta.dart';
10

11
import 'common.dart' show throwToolExit;
12
import 'context.dart';
13
import 'platform.dart';
14
import 'process.dart';
15

16 17
export 'package:file/file.dart';
export 'package:file/local.dart';
yjbanov's avatar
yjbanov committed
18

19
const String _kRecordingType = 'file';
20
const FileSystem _kLocalFs = LocalFileSystem();
21

22
/// Currently active implementation of the file system.
yjbanov's avatar
yjbanov committed
23 24 25
///
/// By default it uses local disk-based implementation. Override this in tests
/// with [MemoryFileSystem].
26
FileSystem get fs => context.get<FileSystem>() ?? _kLocalFs;
27

28 29
/// Gets a [FileSystem] that will record file system activity to the specified
/// base recording [location].
30 31 32 33
///
/// Activity will be recorded in a subdirectory of [location] named `"file"`.
/// It is permissible for [location] to represent an existing non-empty
/// directory as long as there is no collision with the `"file"` subdirectory.
34
RecordingFileSystem 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 45
/// Gets a [FileSystem] that replays invocation activity from a previously
/// recorded set of invocations.
46 47 48
///
/// [location] must represent a directory to which file system activity has
/// been recorded (i.e. the result of having been previously passed to
49 50
/// [getRecordingFileSystem]), or a [ToolExit] will be thrown.
ReplayFileSystem getReplayFileSystem(String location) {
51
  final Directory dir = getReplaySource(location, _kRecordingType);
52
  return ReplayFileSystem(recording: dir);
53 54
}

55 56
/// Create the ancestor directories of a file path if they do not already exist.
void ensureDirectoryExists(String filePath) {
57
  final String dirPath = fs.path.dirname(filePath);
58
  if (fs.isDirectorySync(dirPath))
59
    return;
60 61 62
  try {
    fs.directory(dirPath).createSync(recursive: true);
  } on FileSystemException catch (e) {
63
    throwToolExit('Failed to create directory "$dirPath": ${e.osError.message}');
64
  }
65
}
66

67 68
/// Recursively copies `srcDir` to `destDir`, invoking [onFileCopied] if
/// specified for each source/destination file pair.
69 70
///
/// Creates `destDir` if needed.
71
void 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 76 77

  if (!destDir.existsSync())
    destDir.createSync(recursive: true);

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 86
      copyDirectorySync(
        entity, destDir.fileSystem.directory(newPath));
87
    } else {
88
      throw Exception('${entity.path} is neither File nor Directory');
89
    }
90
  }
91
}
92 93 94 95 96 97 98 99 100 101 102 103

/// Gets a directory to act as a recording destination, creating the directory
/// as necessary.
///
/// The directory will exist in the local file system, be named [basename], and
/// be a child of the directory identified by [dirname].
///
/// If the target directory already exists as a directory, the existing
/// directory must be empty, or a [ToolExit] will be thrown. If the target
/// directory exists as an entity other than a directory, a [ToolExit] will
/// also be thrown.
Directory getRecordingSink(String dirname, String basename) {
104
  final String location = _kLocalFs.path.join(dirname, basename);
105
  switch (_kLocalFs.typeSync(location, followLinks: false)) {
106 107
    case FileSystemEntityType.file:
    case FileSystemEntityType.link:
108 109
      throwToolExit('Invalid record-to location: $dirname ("$basename" exists as non-directory)');
      break;
110
    case FileSystemEntityType.directory:
111 112 113
      if (_kLocalFs.directory(location).listSync(followLinks: false).isNotEmpty)
        throwToolExit('Invalid record-to location: $dirname ("$basename" is not empty)');
      break;
114
    case FileSystemEntityType.notFound:
115 116 117 118 119 120 121 122 123 124 125 126 127
      _kLocalFs.directory(location).createSync(recursive: true);
  }
  return _kLocalFs.directory(location);
}

/// Gets a directory that holds a saved recording to be used for the purpose of
/// replay.
///
/// The directory will exist in the local file system, be named [basename], and
/// be a child of the directory identified by [dirname].
///
/// If the target directory does not exist, a [ToolExit] will be thrown.
Directory getReplaySource(String dirname, String basename) {
128
  final Directory dir = _kLocalFs.directory(_kLocalFs.path.join(dirname, basename));
129 130 131 132
  if (!dir.existsSync())
    throwToolExit('Invalid replay-from location: $dirname ("$basename" does not exist)');
  return dir;
}
133 134 135

/// Canonicalizes [path].
///
136
/// This function implements the behavior of `canonicalize` from
137 138 139 140
/// `package:path`. However, unlike the original, it does not change the ASCII
/// case of the path. Changing the case can break hot reload in some situations,
/// for an example see: https://github.com/flutter/flutter/issues/9539.
String canonicalizePath(String path) => fs.path.normalize(fs.path.absolute(path));
141 142 143 144 145 146

/// Escapes [path].
///
/// On Windows it replaces all '\' with '\\'. On other platforms, it returns the
/// path unchanged.
String escapePath(String path) => platform.isWindows ? path.replaceAll('\\', '\\\\') : path;
147 148 149 150 151 152 153

/// 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.
154
bool isOlderThanReference({ @required FileSystemEntity entity, @required File referenceFile }) {
155 156 157 158 159
  if (!entity.existsSync())
    return true;
  return referenceFile.existsSync()
      && referenceFile.lastModifiedSync().isAfter(entity.statSync().modified);
}