Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/devtools_app_shared/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ resolution: workspace
dependencies:
collection: ^1.15.0
dds_service_extensions: ^2.0.0
devtools_shared: ^13.0.0
devtools_shared: ^14.0.0-wip
dtd: ^4.0.0
flutter:
sdk: flutter
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools_extensions/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ executables:

dependencies:
args: ^2.4.2
devtools_shared: ^13.0.0
devtools_shared: ^14.0.0-wip
devtools_app_shared: ^0.5.1
flutter:
sdk: flutter
Expand Down
24 changes: 23 additions & 1 deletion packages/devtools_shared/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,31 @@ Copyright 2025 The Flutter Authors
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
-->
# 13.0.2-wip
# 14.0.0-wip
* The minimum Dart SDK version is bumped to 3.11.0.
* The minimum Flutter SDK version is bumped to 3.41.0.
* **Breaking changes**: `LocalFileSystem`, an extension which provided some handy
helpers, has been refactored into an extension on the `file` package's
`FileSystem` abstraction. In detail:
* `fileSystem` is a new top-level constant which represents the local (real)
file system.
* `LocalFileSystem.devToolsDir()` is now a static getter,
`FileSystemExtension.devToolsDir`.
* `LocalFileSystem.maybeMoveLegacyDevToolsStore()` is now an instance method,
`FileSystemExtension.maybeMoveLegacyDevToolsStore()`.
* `LocalFileSystem.devToolsStoreLocation()` is now a static getter,
`FileSystemExtension.devToolsStoreLocation`.
* `LocalFileSystem.ensureDevToolsDirectory()` is now private.
* `LocalFileSystem.devToolsFileFromPath()` is now an instance method,
`FileSystemExtension.devToolsFileFromPath()`.
* `LocalFileSystem.devToolsFileAsJson()` is now an instance method,
`FileSystemExtension.devToolsFileAsJson()`.
* `LocalFileSystem.flutterStoreExists()` is now an instance getter,
`FileSystemExtension.flutterStoreExists`.
* `IOPersistentProperties.new` accepts a new optional `FileSystem fs`
argument.
* Update `LocalFileSystem` and `IOPersistentProperties` to use `package:file`
instead of `dart:io` to allow mocking the file system.

# 13.0.1
* Handle null values for `FlutterStore.flutterClientId`.
Expand Down
5 changes: 3 additions & 2 deletions packages/devtools_shared/lib/src/server/devtools_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ enum DevToolsStoreKeys {
/// Provides access to the local DevTools store (~/.flutter-devtools/.devtools).
class DevToolsUsage {
DevToolsUsage() {
LocalFileSystem.maybeMoveLegacyDevToolsStore();
// TODO(srawlins): Accept a FileSystem parameter during tests.
fileSystem.maybeMoveLegacyDevToolsStore();
properties = IOPersistentProperties(
storeName,
documentDirPath: LocalFileSystem.devToolsDir(),
documentDirPath: FileSystemExtension.devToolsDir,
);
_removeLegacyKeys();
}
Comment thread
srawlins marked this conversation as resolved.
Expand Down
121 changes: 66 additions & 55 deletions packages/devtools_shared/lib/src/server/file_system.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,108 +3,117 @@
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

import 'dart:convert';
import 'dart:io';
import 'dart:io' as io show Platform, File;

import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:path/path.dart' as path;

import 'devtools_store.dart';

/// A namespace local file system utlities.
extension LocalFileSystem on Never {
static String _userHomeDir() {
final envKey = Platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME';
return Platform.environment[envKey] ?? '.';
/// The real, local file system, which can be avoided in tests.
const FileSystem fileSystem = LocalFileSystem();

extension FileSystemExtension on FileSystem {
static String get _userHomeDir {
final envKey = io.Platform.operatingSystem == 'windows'
? 'APPDATA'
: 'HOME';
return io.Platform.environment[envKey] ?? '.';
}

/// Returns the path to the DevTools storage directory.
static String devToolsDir() {
return path.join(_userHomeDir(), '.flutter-devtools');
/// The path to the DevTools storage directory.
static String get devToolsDir {
return path.join(_userHomeDir, '.flutter-devtools');
}

/// Moves the .devtools file to ~/.flutter-devtools/.devtools if the .devtools
/// file exists in the user's home directory.
static void maybeMoveLegacyDevToolsStore() {
final file = File(path.join(_userHomeDir(), DevToolsUsage.storeName));
if (file.existsSync()) {
ensureDevToolsDirectory();
file.copySync(devToolsStoreLocation());
file.deleteSync();
/// Moves the `.devtools` file to `~/.flutter-devtools/.devtools` if the
/// `.devtools` file exists in the user's home directory.
void maybeMoveLegacyDevToolsStore() {
final storeFile = file(path.join(_userHomeDir, DevToolsUsage.storeName));
if (storeFile.existsSync()) {
_ensureDevToolsDirectory();
storeFile.copySync(devToolsStoreLocation);
storeFile.deleteSync();
}
}

static String devToolsStoreLocation() {
return path.join(devToolsDir(), DevToolsUsage.storeName);
static String get devToolsStoreLocation {
return path.join(devToolsDir, DevToolsUsage.storeName);
}

/// Creates the ~/.flutter-devtools directory if it does not already exist.
static void ensureDevToolsDirectory() {
Directory(devToolsDir()).createSync();
/// Creates the `~/.flutter-devtools` directory if it does not already exist.
void _ensureDevToolsDirectory() {
directory(devToolsDir).createSync();
}

/// Returns a DevTools file from the given path.
///
/// Only files within ~/.flutter-devtools/ can be accessed.
static File? devToolsFileFromPath(String pathFromDevToolsDir) {
if (pathFromDevToolsDir.contains('..') ||
path.isAbsolute(pathFromDevToolsDir)) {
File? devToolsFileFromPath(String relativePath) {
if (relativePath.contains('..') || path.isAbsolute(relativePath)) {
// The passed in path should not be able to walk up the directory tree
// outside of the ~/.flutter-devtools/ directory. It must also not be an
// absolute path: path.join() discards the base directory when its second
// argument is absolute, which would otherwise allow reading an arbitrary
// file on disk (e.g. an absolute path to a credentials .json file).
// outside of the `~/.flutter-devtools/` directory. It must also not be an
// absolute path: `path.join()` discards the base directory when its
// second argument is absolute, which would otherwise allow reading an
// arbitrary file on disk (e.g. an absolute path to a credentials `.json`
// file).
return null;
}

ensureDevToolsDirectory();
final devToolsDirPath = devToolsDir();
final file = File(path.join(devToolsDirPath, pathFromDevToolsDir));
_ensureDevToolsDirectory();
final targetFile = file(path.join(devToolsDir, relativePath));
// Defense in depth: ensure the resolved path is actually contained within
// the DevTools directory.
if (!path.isWithin(devToolsDirPath, file.path)) {
return null;
}
if (!file.existsSync()) {
return null;
}
return file;
if (!path.isWithin(devToolsDir, targetFile.path)) return null;
if (!targetFile.existsSync()) return null;
return targetFile;
}

/// Returns a DevTools file from the given path as encoded json.
/// Returns a DevTools file from the given path as encoded JSON.
///
/// Only files within ~/.flutter-devtools/ can be accessed.
static String? devToolsFileAsJson(String pathFromDevToolsDir) {
final file = devToolsFileFromPath(pathFromDevToolsDir);
if (file == null) return null;
/// Only files within `~/.flutter-devtools/` can be accessed.
String? devToolsFileAsJson(String relativePath) {
final targetFile = devToolsFileFromPath(relativePath);
if (targetFile == null) return null;

final fileName = path.basename(file.path);
final fileName = path.basename(targetFile.path);
if (!fileName.endsWith('.json')) return null;

final content = file.readAsStringSync();
final content = targetFile.readAsStringSync();
final json = jsonDecode(content) as Map;
json['lastModifiedTime'] = file.lastModifiedSync().toString();
json['lastModifiedTime'] = targetFile.lastModifiedSync().toString();
Comment thread
srawlins marked this conversation as resolved.
return jsonEncode(json);
}

/// Whether the flutter store file exists.
static bool flutterStoreExists() {
final flutterStore = File(path.join(_userHomeDir(), '.flutter'));
bool get flutterStoreExists {
final flutterStore = file(path.join(_userHomeDir, '.flutter'));
return flutterStore.existsSync();
}
}

class IOPersistentProperties {
IOPersistentProperties(this.name, {String? documentDirPath}) {
IOPersistentProperties(
this.name, {
String? documentDirPath,
FileSystem fs = fileSystem,
}) : _fs = fs {
final fileName = name.replaceAll(' ', '_');
documentDirPath ??= LocalFileSystem._userHomeDir();
_file = File(path.join(documentDirPath, fileName));
documentDirPath ??= FileSystemExtension._userHomeDir;
_file = _fs.file(path.join(documentDirPath, fileName));
if (!_file.existsSync()) {
_file.createSync(recursive: true);
}
syncSettings();
}

IOPersistentProperties.fromFile(File file) : name = path.basename(file.path) {
_file = file;
// TODO(srawlins): This is unused in any devtools code. Even in tests. Either
// test it, if it is needed by users, or remove it.
IOPersistentProperties.fromFile(io.File file)
: name = path.basename(file.path),
_fs = file is File ? file.fileSystem : fileSystem {
_file = file is File ? file : _fs.file(file.path);
if (!_file.existsSync()) {
_file.createSync(recursive: true);
}
Expand All @@ -113,7 +122,9 @@ class IOPersistentProperties {

final String name;

late File _file;
final FileSystem _fs;

late final File _file;

late Map<String, Object?> _map;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ extension _AppSizeHandler on Never {
if (missingRequiredParams != null) return missingRequiredParams;

final filePath = queryParams[AppSizeApi.baseAppSizeFilePropertyName]!;
final fileJson = LocalFileSystem.devToolsFileAsJson(filePath);
final fileJson = fileSystem.devToolsFileAsJson(filePath);
if (fileJson == null) {
return api.badRequest('No JSON file available at $filePath.');
}
Expand All @@ -39,7 +39,7 @@ extension _AppSizeHandler on Never {
if (missingRequiredParams != null) return missingRequiredParams;

final filePath = queryParams[AppSizeApi.testAppSizeFilePropertyName]!;
final fileJson = LocalFileSystem.devToolsFileAsJson(filePath);
final fileJson = fileSystem.devToolsFileAsJson(filePath);
if (fileJson == null) {
return api.badRequest('No JSON file available at $filePath.');
}
Expand Down
10 changes: 4 additions & 6 deletions packages/devtools_shared/lib/src/server/server_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,7 @@ class ServerApi {
// ----- Flutter Tool GA store. -----
case apiGetFlutterGAClientId:
return _encodeResponse(
LocalFileSystem.flutterStoreExists()
? _flutterStore.flutterClientId
: '',
fileSystem.flutterStoreExists ? _flutterStore.flutterClientId : '',
api: api,
);

Expand Down Expand Up @@ -222,16 +220,16 @@ class ServerApi {
: null;
}

/// Accessing DevTools store file e.g., ~/.flutter-devtools/.devtools
/// Accessing DevTools store file e.g., `~/.flutter-devtools/.devtools`.
static final _devToolsStore = DevToolsUsage();

/// Accessing Flutter store file e.g., ~/.flutter
/// Accessing Flutter store file e.g., `~/.flutter`.
static final _flutterStore = FlutterStore();

static DevToolsUsage get devToolsPreferences => _devToolsStore;

/// Provides read and write access to DevTools options files
/// (e.g. path/to/app/root/devtools_options.yaml).
/// (e.g. `path/to/app/root/devtools_options.yaml`).
static final _devToolsOptions = DevToolsOptions();

/// Logs a page view in the DevTools server.
Expand Down
3 changes: 2 additions & 1 deletion packages/devtools_shared/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
name: devtools_shared
description: Package of shared Dart structures between devtools_app, dds, and other tools.

version: 13.0.2-dev
version: 14.0.0-wip

repository: https://github.com/flutter/devtools/tree/master/packages/devtools_shared

Expand All @@ -18,6 +18,7 @@ dependencies:
collection: ^1.15.0
dtd: ^4.0.0
extension_discovery: ^2.1.0
file: ^7.0.0
meta: ^1.9.1
path: ^1.8.0
shelf: ^1.1.0
Expand Down
Loading
Loading