config.dart 6.41 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
    String name, {
    required FileSystem fileSystem,
    required Logger logger,
    required Platform platform
  }) {
    return Config._common(
      name,
      fileSystem: fileSystem,
      logger: logger,
41
      platform: platform
42 43 44
    );
  }

45
  /// Similar to the default config constructor, but with some different
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
  /// 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(
68
    String name, {
69 70 71
    required FileSystem fileSystem,
    required Logger logger,
    required Platform platform,
72
    bool managed = false
73
  }) {
74 75 76
    final String filePath = _configPath(platform, fileSystem, name);
    final File file = fileSystem.file(filePath);
    file.parent.createSync(recursive: true);
77
    return Config.createForTesting(file, logger, managed: managed);
78 79 80 81
  }

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

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

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

      if (managed) {
        rethrow;
      }
136
    }
137 138
  }

139 140 141 142 143 144 145 146 147 148 149 150 151 152
  /// 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.
153
  static const String kXdgConfigFallback = '.config';
154

155
  /// The default name for the Flutter config file.
156
  static const String kFlutterSettings = 'settings';
157 158 159

  final Logger _logger;

160 161
  File _file;

162
  String get configPath => _file.path;
163

164
  Map<String, dynamic> _values = <String, Object>{};
165 166 167

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

168 169
  bool containsKey(String key) => _values.containsKey(key);

170
  Object? getValue(String key) => _values[key];
171

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

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

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

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

  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;
  }
212
}