blob: b14f9abf2955a99734d1070288cc085f529410e8 [file] [log] [blame]
// 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;
}
}