config.dart 6.34 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
        _file.deleteSync();
      }
121 122 123 124
    } on Exception catch (err) {
      _logger
        ..printError('Could not read preferences in ${file.path}.\n$err')
        ..printError(
125 126
          'You may need to resolve the error above and reapply any previously '
          'saved configuration with the "flutter config" command.',
127
        );
128 129 130 131

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

135 136 137 138 139 140 141 142 143 144 145 146 147 148
  /// 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.
149
  static const String kXdgConfigFallback = '.config';
150

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

  final Logger _logger;

156 157
  File _file;

158
  String get configPath => _file.path;
159

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

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

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

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

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

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

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

  // 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) {
190
    final String envKey = platform.isWindows ? 'APPDATA' : 'HOME';
191 192
    return platform.environment[envKey] ?? '.';
  }
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207

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