// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:file/memory.dart'; import 'package:meta/meta.dart'; import '../convert.dart'; import 'error_handling_io.dart'; import 'file_system.dart'; import 'logger.dart'; import 'platform.dart'; import 'utils.dart'; /// A class to abstract configuration files. class Config { /// Constructs a new [Config] object from a file called [name] in the /// 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. /// /// 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 factory Config( String name, { required FileSystem fileSystem, required Logger logger, required Platform platform }) { return Config._common( name, fileSystem: fileSystem, logger: logger, platform: platform ); } /// Similar 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( String name, { required FileSystem fileSystem, required Logger logger, required Platform platform, bool managed = false }) { final String filePath = _configPath(platform, fileSystem, name); final File file = fileSystem.file(filePath); file.parent.createSync(recursive: true); return Config.createForTesting(file, logger, managed: managed); } /// Constructs a new [Config] object from a file called [name] in /// the given [Directory]. /// /// Defaults to [BufferLogger], [MemoryFileSystem], and [name]=test. factory Config.test({ String name = 'test', Directory? directory, Logger? logger, bool managed = false }) { directory ??= MemoryFileSystem.test().directory('/'); return Config.createForTesting( directory.childFile('.${kConfigDir}_$name'), logger ?? BufferLogger.test(), managed: managed ); } /// Test only access to the Config constructor. @visibleForTesting Config.createForTesting(File file, Logger logger, {bool managed = false}) : _file = file, _logger = logger { if (!_file.existsSync()) { return; } try { ErrorHandlingFileSystem.noExitOnFailure(() { _values = castStringKeyedMap(json.decode(_file.readAsStringSync())) ?? <String, Object>{}; }); } on FormatException { _logger ..printError('Failed to decode preferences in ${_file.path}.') ..printError( 'You may need to reapply any previously saved configuration ' 'with the "flutter config" command.', ); if (managed) { rethrow; } else { _file.deleteSync(); } } on Exception catch (err) { _logger ..printError('Could not read preferences in ${file.path}.\n$err') ..printError( 'You may need to resolve the error above and reapply any previously ' 'saved configuration with the "flutter config" command.', ); if (managed) { rethrow; } } } /// 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. static const String kXdgConfigFallback = '.config'; /// The default name for the Flutter config file. static const String kFlutterSettings = 'settings'; final Logger _logger; File _file; String get configPath => _file.path; Map<String, dynamic> _values = <String, Object>{}; Iterable<String> get keys => _values.keys; bool containsKey(String key) => _values.containsKey(key); Object? getValue(String key) => _values[key]; void setValue(String key, Object value) { _values[key] = value; _flushValues(); } void removeValue(String key) { _values.remove(key); _flushValues(); } void _flushValues() { String json = const JsonEncoder.withIndent(' ').convert(_values); json = '$json\n'; _file.writeAsStringSync(json); } // 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) { final String envKey = platform.isWindows ? 'APPDATA' : 'HOME'; return platform.environment[envKey] ?? '.'; } 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; } }