config.dart 6.36 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/memory.dart';
6 7
import 'package:meta/meta.dart';

8
import '../convert.dart';
9
import 'error_handling_io.dart';
10
import 'file_system.dart';
11
import 'logger.dart';
12
import 'platform.dart';
13
import 'utils.dart';
14

15
/// A class to abstract configuration files.
16
class Config {
17
  /// Constructs a new [Config] object from a file called [name] in the
18 19 20 21 22 23 24 25
  /// current user's configuration directory as determined by the [Platform]
  /// and [FileSystem].
  ///
  /// The configuration directory defaults to $XDG_CONFIG_HOME on Linux and
  /// macOS, but falls back to the home directory if a file named
  /// `.flutter_$name` already exists there. On other platforms the
  /// configuration file will always be a file named `.flutter_$name` in the
  /// home directory.
26 27 28 29 30
  ///
  /// Uses some good default behaviours:
  /// - deletes the file if it's not valid JSON
  /// - reports an empty config in that case
  /// - logs and catches any exceptions
31
  factory Config(
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
    String name, {
    required FileSystem fileSystem,
    required Logger logger,
    required Platform platform
  }) {
    return Config._common(
      name,
      fileSystem: fileSystem,
      logger: logger,
      platform: platform,
      managed: false
    );
  }

  /// Similiar to the default config constructor, but with some different
  /// behaviours:
  /// - will not delete the config if it's not valid JSON
  /// - will log but also rethrow any exceptions while loading the JSON, so
  ///   you can actually detect whether something went wrong
  ///
  /// Useful if you want some more control.
  factory Config.managed(
    String name, {
    required FileSystem fileSystem,
    required Logger logger,
    required Platform platform
  }) {
    return Config._common(
      name,
      fileSystem: fileSystem,
      logger: logger,
      platform: platform,
      managed: true
    );
  }

  factory Config._common(
69
    String name, {
70 71 72
    required FileSystem fileSystem,
    required Logger logger,
    required Platform platform,
73
    bool managed = false
74
  }) {
75 76 77
    final String filePath = _configPath(platform, fileSystem, name);
    final File file = fileSystem.file(filePath);
    file.parent.createSync(recursive: true);
78
    return Config.createForTesting(file, logger, managed: managed);
79 80 81 82
  }

  /// Constructs a new [Config] object from a file called [name] in
  /// the given [Directory].
83 84 85 86
  ///
  /// Defaults to [BufferLogger], [MemoryFileSystem], and [name]=test.
  factory Config.test({
    String name = 'test',
87 88
    Directory? directory,
    Logger? logger,
89
    bool managed = false
90 91
  }) {
    directory ??= MemoryFileSystem.test().directory('/');
92 93 94
    return Config.createForTesting(
      directory.childFile('.${kConfigDir}_$name'),
      logger ?? BufferLogger.test(),
95
      managed: managed
96
    );
97
  }
98

99 100
  /// Test only access to the Config constructor.
  @visibleForTesting
101
  Config.createForTesting(File file, Logger logger, {bool managed = false}) : _file = file, _logger = logger {
102 103 104 105
    if (!_file.existsSync()) {
      return;
    }
    try {
106
      ErrorHandlingFileSystem.noExitOnFailure(() {
107
        _values = castStringKeyedMap(json.decode(_file.readAsStringSync())) ?? <String, Object>{};
108
      });
109 110 111 112
    } on FormatException {
      _logger
        ..printError('Failed to decode preferences in ${_file.path}.')
        ..printError(
113 114
          'You may need to reapply any previously saved configuration '
          'with the "flutter config" command.',
115
        );
116

117 118 119
      if (managed) {
        rethrow;
      } else {
120 121
        _file.deleteSync();
      }
122 123 124 125
    } on Exception catch (err) {
      _logger
        ..printError('Could not read preferences in ${file.path}.\n$err')
        ..printError(
126 127
          'You may need to resolve the error above and reapply any previously '
          'saved configuration with the "flutter config" command.',
128
        );
129 130 131 132

      if (managed) {
        rethrow;
      }
133
    }
134 135
  }

136 137 138 139 140 141 142 143 144 145 146 147 148 149
  /// The default directory name for Flutter's configs.

  /// Configs will be written to the user's config path. If there is already a
  /// file with the name `.${kConfigDir}_$name` in the user's home path, that
  /// file will be used instead.
  static const String kConfigDir = 'flutter';

  /// Environment variable specified in the XDG Base Directory
  /// [specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
  /// to specify the user's configuration directory.
  static const String kXdgConfigHome = 'XDG_CONFIG_HOME';

  /// Fallback directory in the user's home directory if `XDG_CONFIG_HOME` is
  /// not defined.
150
  static const String kXdgConfigFallback = '.config';
151

152
  /// The default name for the Flutter config file.
153
  static const String kFlutterSettings = 'settings';
154 155 156

  final Logger _logger;

157 158
  File _file;

159
  String get configPath => _file.path;
160

161
  Map<String, dynamic> _values = <String, Object>{};
162 163 164

  Iterable<String> get keys => _values.keys;

165 166
  bool containsKey(String key) => _values.containsKey(key);

167
  Object? getValue(String key) => _values[key];
168

169
  void setValue(String key, Object value) {
170 171 172 173 174 175 176 177 178 179
    _values[key] = value;
    _flushValues();
  }

  void removeValue(String key) {
    _values.remove(key);
    _flushValues();
  }

  void _flushValues() {
180
    String json = const JsonEncoder.withIndent('  ').convert(_values);
181
    json = '$json\n';
182
    _file.writeAsStringSync(json);
183
  }
184 185 186 187 188 189 190

  // Reads the process environment to find the current user's home directory.
  //
  // If the searched environment variables are not set, '.' is returned instead.
  //
  // Note that this is different from FileSystemUtils.homeDirPath.
  static String _userHomePath(Platform platform) {
191
    final String envKey = platform.isWindows ? 'APPDATA' : 'HOME';
192 193
    return platform.environment[envKey] ?? '.';
  }
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208

  static String _configPath(
      Platform platform, FileSystem fileSystem, String name) {
    final String homeDirFile =
        fileSystem.path.join(_userHomePath(platform), '.${kConfigDir}_$name');
    if (platform.isLinux || platform.isMacOS) {
      if (fileSystem.isFileSync(homeDirFile)) {
        return homeDirFile;
      }
      final String configDir = platform.environment[kXdgConfigHome] ??
          fileSystem.path.join(_userHomePath(platform), '.config', kConfigDir);
      return fileSystem.path.join(configDir, name);
    }
    return homeDirFile;
  }
209
}