From 4d7c1854601e8918501769344757000288d131ae Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Wed, 18 Feb 2026 07:43:21 +0100 Subject: [PATCH] migrate mozilla caches into profiles --- app/lib/core/filesystem.dart | 118 ++++++------------ .../dialogs/switch_profile_dialog.dart | 80 ++++-------- .../utils/profile_switch_handler.dart | 15 +-- .../user/domain/repositories/profile.dart | 6 - app/lib/utils/filesystem.dart | 50 ++------ 5 files changed, 75 insertions(+), 194 deletions(-) diff --git a/app/lib/core/filesystem.dart b/app/lib/core/filesystem.dart index 8346ca2d..be5ac31b 100644 --- a/app/lib/core/filesystem.dart +++ b/app/lib/core/filesystem.dart @@ -20,13 +20,12 @@ import 'dart:async'; import 'dart:io'; -import 'package:collection/collection.dart'; -import 'package:nullability/nullability.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart' as path_provider; import 'package:sqlite3/sqlite3.dart'; import 'package:uuid/uuid.dart'; +import 'package:weblibre/core/logger.dart'; import 'package:weblibre/domain/entities/profile.dart'; import 'package:weblibre/utils/filesystem.dart' as fs; @@ -67,94 +66,56 @@ class _Filesystem { } Future clearMozillaProfileCache(String profileId) { - return fs.clearMozillaProfileCache(profileId); + return fs.clearMozillaProfileCache(selectedProfileDir, profileId); } List getMozillaProfileIds(UuidValue uuid) { return fs.getMozillaProfileIds(getProfileDir(uuid)); } - Future checkForDuplicateMozillaProfile(UuidValue profile) async { - final duplicates = await fs - .getProfilesWithDuplicateMozillaProfiles(profilesDir) - .then((dirs) => dirs.map((dir) => dir.path).toList()); - final profileDir = fs.getProfileDir(profilesDir, profile).path; + /// If the old canonical location `{profile}/mozilla/` exists as a real + /// directory and the new location `{profile}/files/mozilla/` does not, + /// rename the former to the latter. + Future _migrateMozillaDirToFiles() async { + final oldDir = Directory(p.join(selectedProfileDir.path, 'mozilla')); + final newDir = Directory( + p.join(selectedProfileDir.path, 'files', 'mozilla'), + ); - return duplicates - .firstWhereOrNull((dir) => p.isWithin(profileDir, dir)) - .mapNotNull((dir) => p.basename(dir)); - } - - Future _linkMozillaDir(Directory filesDir) async { - final mozillaDir = Directory(p.join(selectedProfileDir.path, 'mozilla')); - await mozillaDir.create(recursive: true); - - final mozillaPath = p.join(filesDir.path, 'mozilla'); - - Future moveAside(FileSystemEntityType type) async { - final backupPath = p.join( - filesDir.path, - 'mozilla.backup.${DateTime.now().millisecondsSinceEpoch}', - ); - - if (type == FileSystemEntityType.directory) { - await Directory(mozillaPath).rename(backupPath); - } else { - await File(mozillaPath).rename(backupPath); - } - } - - Future createLink() async { - await Link(mozillaPath).create(mozillaDir.path); - } - - final currentType = await FileSystemEntity.type( - mozillaPath, + final oldType = await FileSystemEntity.type( + oldDir.path, followLinks: false, ); - switch (currentType) { - case FileSystemEntityType.notFound: - break; - case FileSystemEntityType.link: - final link = Link(mozillaPath); - try { - if (await link.target() == mozillaDir.path) { - return; - } - } on FileSystemException { - // Replace unreadable or broken links. - } - - await link.delete(); - case FileSystemEntityType.directory: - case FileSystemEntityType.file: - case FileSystemEntityType.unixDomainSock: - case FileSystemEntityType.pipe: - default: - await moveAside(currentType); + if (oldType == FileSystemEntityType.directory && !await newDir.exists()) { + await Directory( + p.join(selectedProfileDir.path, 'files'), + ).create(recursive: true); + await oldDir.rename(newDir.path); } + } - try { - await createLink(); - } on PathExistsException { - final retryType = await FileSystemEntity.type( - mozillaPath, - followLinks: false, + Future _migrateGeckoCache() async { + final profileIds = fs.getMozillaProfileIds(selectedProfileDir); + final globalCacheDir = Directory(p.join(dataDir.path, 'cache')); + + for (final profileId in profileIds) { + final oldCache = Directory(p.join(globalCacheDir.path, profileId)); + final newCache = Directory( + p.join(selectedProfileDir.path, 'cache', profileId), ); - if (retryType == FileSystemEntityType.link) { - final link = Link(mozillaPath); - if (await link.target() == mozillaDir.path) { - return; + if (await oldCache.exists() && !await newCache.exists()) { + try { + await oldCache.rename(newCache.path); + } catch (e, s) { + logger.w( + 'Failed to migrate Gecko cache for $profileId', + error: e, + stackTrace: s, + ); } - - await link.delete(); - } else if (retryType != FileSystemEntityType.notFound) { - await moveAside(retryType); } - - await createLink(); } } @@ -233,7 +194,8 @@ class _Filesystem { ); await profileDatabasesDir.create(); - await _linkMozillaDir(filesDir); + await _migrateMozillaDirToFiles(); + await _migrateGeckoCache(); await _setupSqliteCache(); } @@ -244,8 +206,10 @@ class _Filesystem { ) async { final profileDir = getProfileDir(defaultProfile.uuidValue); - final newMozillaDir = Directory(p.join(profileDir.path, 'mozilla')); - await newMozillaDir.create(); + final newMozillaDir = Directory( + p.join(profileDir.path, 'files', 'mozilla'), + ); + await newMozillaDir.create(recursive: true); await mozillaDir.rename(newMozillaDir.path); await _copyDirectory( diff --git a/app/lib/features/user/domain/presentation/dialogs/switch_profile_dialog.dart b/app/lib/features/user/domain/presentation/dialogs/switch_profile_dialog.dart index bd3692e9..2b32285e 100644 --- a/app/lib/features/user/domain/presentation/dialogs/switch_profile_dialog.dart +++ b/app/lib/features/user/domain/presentation/dialogs/switch_profile_dialog.dart @@ -18,70 +18,38 @@ * along with this program. If not, see . */ import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; -/// Result of the switch profile dialog. -/// - `shouldSwitch`: Whether the user confirmed the switch. -/// - `clearCache`: Whether to clear the shared cache. -typedef SwitchProfileDialogResult = ({bool shouldSwitch, bool clearCache}); - /// Shows a confirmation dialog for switching user profiles. /// -/// Returns a [SwitchProfileDialogResult] if the user confirms, or null if dismissed. -/// If [duplicateMozillaProfile] is provided, shows an option to clear shared cache. -Future showSwitchProfileDialog( +/// Returns `true` if the user confirms the switch, or null if dismissed. +Future showSwitchProfileDialog( BuildContext context, { required String profileName, - String? duplicateMozillaProfile, }) { - return showDialog( + return showDialog( context: context, - builder: (context) => HookBuilder( - builder: (context) { - final clearCache = useState(false); - - return AlertDialog( - icon: const Icon(Icons.warning), - title: const Text('Switch User'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "Switching to User '$profileName' will require a restart of the Browser.", - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - if (duplicateMozillaProfile != null) - SwitchListTile( - contentPadding: EdgeInsets.zero, - value: clearCache.value, - title: const Text('Clear Shared Cache'), - subtitle: const Text( - 'This User has been created based on an exisiting Mozilla Profile Identifier. Clearing cache will affect all linked accounts.', - ), - onChanged: (value) { - clearCache.value = value; - }, - ), - ], - ), - actions: [ - TextButton( - onPressed: () { - context.pop((shouldSwitch: false, clearCache: false)); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - context.pop((shouldSwitch: true, clearCache: clearCache.value)); - }, - child: const Text('Switch Profile'), - ), - ], - ); - }, + builder: (context) => AlertDialog( + icon: const Icon(Icons.warning), + title: const Text('Switch User'), + content: Text( + "Switching to User '$profileName' will require a restart of the Browser.", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + actions: [ + TextButton( + onPressed: () { + context.pop(false); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + context.pop(true); + }, + child: const Text('Switch Profile'), + ), + ], ), ); } diff --git a/app/lib/features/user/domain/presentation/utils/profile_switch_handler.dart b/app/lib/features/user/domain/presentation/utils/profile_switch_handler.dart index 54e910d2..74ef9dd7 100644 --- a/app/lib/features/user/domain/presentation/utils/profile_switch_handler.dart +++ b/app/lib/features/user/domain/presentation/utils/profile_switch_handler.dart @@ -26,12 +26,11 @@ import 'package:weblibre/features/user/domain/repositories/profile.dart'; import 'package:weblibre/utils/exit_app.dart'; import 'package:weblibre/utils/ui_helper.dart' as ui_helper; -/// Handles the profile switching flow with confirmation dialog and cache clearing options. +/// Handles the profile switching flow with confirmation dialog. /// /// This function: /// - Checks if the profile is already active /// - Shows a confirmation dialog with browser restart warning -/// - Optionally clears shared cache for duplicate Mozilla profiles /// - Switches to the selected profile and exits the app Future handleSwitchProfile( BuildContext context, @@ -48,22 +47,14 @@ Future handleSwitchProfile( return; } - final duplicateMozillaProfile = await filesystem - .checkForDuplicateMozillaProfile(profile.uuidValue); - if (!context.mounted) return; - final result = await showSwitchProfileDialog( + final shouldSwitch = await showSwitchProfileDialog( context, profileName: profile.name, - duplicateMozillaProfile: duplicateMozillaProfile, ); - if (result?.shouldSwitch == true) { - if (duplicateMozillaProfile != null && result?.clearCache == true) { - await filesystem.clearMozillaProfileCache(duplicateMozillaProfile); - } - + if (shouldSwitch == true) { await ref .read(profileRepositoryProvider.notifier) .switchProfile(profile.id); diff --git a/app/lib/features/user/domain/repositories/profile.dart b/app/lib/features/user/domain/repositories/profile.dart index e046da2c..97549685 100644 --- a/app/lib/features/user/domain/repositories/profile.dart +++ b/app/lib/features/user/domain/repositories/profile.dart @@ -65,12 +65,6 @@ class ProfileRepository extends _$ProfileRepository { return false; } - // Clean up Mozilla cache directories before deleting the profile - final mozillaProfileIds = filesystem.getMozillaProfileIds(uuid); - for (final profileId in mozillaProfileIds) { - await filesystem.clearMozillaProfileCache(profileId); - } - await filesystem.getProfileDir(uuid).delete(recursive: true); ref.invalidateSelf(); diff --git a/app/lib/utils/filesystem.dart b/app/lib/utils/filesystem.dart index b6307649..0e9b7b7b 100644 --- a/app/lib/utils/filesystem.dart +++ b/app/lib/utils/filesystem.dart @@ -22,11 +22,9 @@ import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid_value.dart'; import 'package:weblibre/core/logger.dart'; import 'package:weblibre/domain/entities/profile.dart'; -import 'package:weblibre/extensions/iterable.dart'; const profilesDirName = 'weblibre_profiles'; const profileDirPrefix = 'profile-'; @@ -44,30 +42,15 @@ final profileTransformer = }, ); -final profileMozillaDirectoryTransformer = - StreamTransformer.fromHandlers( - handleData: (entity, sink) { - final mozillaDir = Directory(p.join(entity.path, 'mozilla')); - - if (mozillaDir.existsSync()) { - for (final entity in mozillaDir.listSync()) { - if (entity is Directory) { - final profile = p.basename(entity.path); - if (profile.endsWith('.default')) { - sink.add(entity); - } - } - } - } - }, - ); - Future> getAvailableProfileDirectories(Directory profilesDir) { return profilesDir.list().transform(profileTransformer).toList(); } -Future clearMozillaProfileCache(String profileId) async { - final cacheDir = await getApplicationCacheDirectory(); +Future clearMozillaProfileCache( + Directory profileDir, + String profileId, +) async { + final cacheDir = Directory(p.join(profileDir.path, 'cache')); final mozillaCacheDir = Directory(p.join(cacheDir.path, profileId)); if (await mozillaCacheDir.exists()) { @@ -76,9 +59,9 @@ Future clearMozillaProfileCache(String profileId) async { } /// Returns the list of Mozilla profile IDs (`.default` directory names) -/// inside the given profile directory's `mozilla/` subdirectory. +/// inside the given profile directory's `files/mozilla/` subdirectory. List getMozillaProfileIds(Directory profileDir) { - final mozillaDir = Directory(p.join(profileDir.path, 'mozilla')); + final mozillaDir = Directory(p.join(profileDir.path, 'files', 'mozilla')); if (!mozillaDir.existsSync()) return []; return mozillaDir @@ -89,25 +72,6 @@ List getMozillaProfileIds(Directory profileDir) { .toList(); } -Future> getProfilesWithDuplicateMozillaProfiles( - Directory profilesDir, -) async { - final mozillaProfileDirs = await profilesDir - .list() - .transform(profileTransformer) - .transform(profileMozillaDirectoryTransformer) - .toList(); - - final duplicates = mozillaProfileDirs - .map((dir) => p.basename(dir.path)) - .findDuplicates() - .toSet(); - - return mozillaProfileDirs - .where((dir) => duplicates.contains(p.basename(dir.path))) - .toList(); -} - Future readStartupProfile(Directory dir) async { final file = File(p.join(dir.path, _startupProfileFileName));