migrate mozilla caches into profiles

This commit is contained in:
Fabian Freund
2026-02-18 07:43:21 +01:00
parent ccab46384f
commit 4d7c185460
5 changed files with 75 additions and 194 deletions
+41 -77
View File
@@ -20,13 +20,12 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart';
import 'package:nullability/nullability.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart' as path_provider; import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3/sqlite3.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:weblibre/core/logger.dart';
import 'package:weblibre/domain/entities/profile.dart'; import 'package:weblibre/domain/entities/profile.dart';
import 'package:weblibre/utils/filesystem.dart' as fs; import 'package:weblibre/utils/filesystem.dart' as fs;
@@ -67,94 +66,56 @@ class _Filesystem {
} }
Future<void> clearMozillaProfileCache(String profileId) { Future<void> clearMozillaProfileCache(String profileId) {
return fs.clearMozillaProfileCache(profileId); return fs.clearMozillaProfileCache(selectedProfileDir, profileId);
} }
List<String> getMozillaProfileIds(UuidValue uuid) { List<String> getMozillaProfileIds(UuidValue uuid) {
return fs.getMozillaProfileIds(getProfileDir(uuid)); return fs.getMozillaProfileIds(getProfileDir(uuid));
} }
Future<String?> checkForDuplicateMozillaProfile(UuidValue profile) async { /// If the old canonical location `{profile}/mozilla/` exists as a real
final duplicates = await fs /// directory and the new location `{profile}/files/mozilla/` does not,
.getProfilesWithDuplicateMozillaProfiles(profilesDir) /// rename the former to the latter.
.then((dirs) => dirs.map((dir) => dir.path).toList()); Future<void> _migrateMozillaDirToFiles() async {
final profileDir = fs.getProfileDir(profilesDir, profile).path; final oldDir = Directory(p.join(selectedProfileDir.path, 'mozilla'));
final newDir = Directory(
p.join(selectedProfileDir.path, 'files', 'mozilla'),
);
return duplicates final oldType = await FileSystemEntity.type(
.firstWhereOrNull((dir) => p.isWithin(profileDir, dir)) oldDir.path,
.mapNotNull((dir) => p.basename(dir));
}
Future<void> _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<void> 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<void> createLink() async {
await Link(mozillaPath).create(mozillaDir.path);
}
final currentType = await FileSystemEntity.type(
mozillaPath,
followLinks: false, followLinks: false,
); );
switch (currentType) { if (oldType == FileSystemEntityType.directory && !await newDir.exists()) {
case FileSystemEntityType.notFound: await Directory(
break; p.join(selectedProfileDir.path, 'files'),
case FileSystemEntityType.link: ).create(recursive: true);
final link = Link(mozillaPath); await oldDir.rename(newDir.path);
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);
} }
}
try { Future<void> _migrateGeckoCache() async {
await createLink(); final profileIds = fs.getMozillaProfileIds(selectedProfileDir);
} on PathExistsException { final globalCacheDir = Directory(p.join(dataDir.path, 'cache'));
final retryType = await FileSystemEntity.type(
mozillaPath, for (final profileId in profileIds) {
followLinks: false, final oldCache = Directory(p.join(globalCacheDir.path, profileId));
final newCache = Directory(
p.join(selectedProfileDir.path, 'cache', profileId),
); );
if (retryType == FileSystemEntityType.link) { if (await oldCache.exists() && !await newCache.exists()) {
final link = Link(mozillaPath); try {
if (await link.target() == mozillaDir.path) { await oldCache.rename(newCache.path);
return; } 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 profileDatabasesDir.create();
await _linkMozillaDir(filesDir); await _migrateMozillaDirToFiles();
await _migrateGeckoCache();
await _setupSqliteCache(); await _setupSqliteCache();
} }
@@ -244,8 +206,10 @@ class _Filesystem {
) async { ) async {
final profileDir = getProfileDir(defaultProfile.uuidValue); final profileDir = getProfileDir(defaultProfile.uuidValue);
final newMozillaDir = Directory(p.join(profileDir.path, 'mozilla')); final newMozillaDir = Directory(
await newMozillaDir.create(); p.join(profileDir.path, 'files', 'mozilla'),
);
await newMozillaDir.create(recursive: true);
await mozillaDir.rename(newMozillaDir.path); await mozillaDir.rename(newMozillaDir.path);
await _copyDirectory( await _copyDirectory(
@@ -18,70 +18,38 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.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. /// Shows a confirmation dialog for switching user profiles.
/// ///
/// Returns a [SwitchProfileDialogResult] if the user confirms, or null if dismissed. /// Returns `true` if the user confirms the switch, or null if dismissed.
/// If [duplicateMozillaProfile] is provided, shows an option to clear shared cache. Future<bool?> showSwitchProfileDialog(
Future<SwitchProfileDialogResult?> showSwitchProfileDialog(
BuildContext context, { BuildContext context, {
required String profileName, required String profileName,
String? duplicateMozillaProfile,
}) { }) {
return showDialog<SwitchProfileDialogResult>( return showDialog<bool>(
context: context, context: context,
builder: (context) => HookBuilder( builder: (context) => AlertDialog(
builder: (context) { icon: const Icon(Icons.warning),
final clearCache = useState(false); title: const Text('Switch User'),
content: Text(
return AlertDialog( "Switching to User '$profileName' will require a restart of the Browser.",
icon: const Icon(Icons.warning), style: const TextStyle(fontWeight: FontWeight.bold),
title: const Text('Switch User'), ),
content: Column( actions: [
mainAxisSize: MainAxisSize.min, TextButton(
children: [ onPressed: () {
Text( context.pop(false);
"Switching to User '$profileName' will require a restart of the Browser.", },
style: const TextStyle(fontWeight: FontWeight.bold), child: const Text('Cancel'),
), ),
const SizedBox(height: 8), TextButton(
if (duplicateMozillaProfile != null) onPressed: () {
SwitchListTile( context.pop(true);
contentPadding: EdgeInsets.zero, },
value: clearCache.value, child: const Text('Switch Profile'),
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'),
),
],
);
},
), ),
); );
} }
@@ -26,12 +26,11 @@ import 'package:weblibre/features/user/domain/repositories/profile.dart';
import 'package:weblibre/utils/exit_app.dart'; import 'package:weblibre/utils/exit_app.dart';
import 'package:weblibre/utils/ui_helper.dart' as ui_helper; 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: /// This function:
/// - Checks if the profile is already active /// - Checks if the profile is already active
/// - Shows a confirmation dialog with browser restart warning /// - 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 /// - Switches to the selected profile and exits the app
Future<void> handleSwitchProfile( Future<void> handleSwitchProfile(
BuildContext context, BuildContext context,
@@ -48,22 +47,14 @@ Future<void> handleSwitchProfile(
return; return;
} }
final duplicateMozillaProfile = await filesystem
.checkForDuplicateMozillaProfile(profile.uuidValue);
if (!context.mounted) return; if (!context.mounted) return;
final result = await showSwitchProfileDialog( final shouldSwitch = await showSwitchProfileDialog(
context, context,
profileName: profile.name, profileName: profile.name,
duplicateMozillaProfile: duplicateMozillaProfile,
); );
if (result?.shouldSwitch == true) { if (shouldSwitch == true) {
if (duplicateMozillaProfile != null && result?.clearCache == true) {
await filesystem.clearMozillaProfileCache(duplicateMozillaProfile);
}
await ref await ref
.read(profileRepositoryProvider.notifier) .read(profileRepositoryProvider.notifier)
.switchProfile(profile.id); .switchProfile(profile.id);
@@ -65,12 +65,6 @@ class ProfileRepository extends _$ProfileRepository {
return false; 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); await filesystem.getProfileDir(uuid).delete(recursive: true);
ref.invalidateSelf(); ref.invalidateSelf();
+7 -43
View File
@@ -22,11 +22,9 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid_value.dart'; import 'package:uuid/uuid_value.dart';
import 'package:weblibre/core/logger.dart'; import 'package:weblibre/core/logger.dart';
import 'package:weblibre/domain/entities/profile.dart'; import 'package:weblibre/domain/entities/profile.dart';
import 'package:weblibre/extensions/iterable.dart';
const profilesDirName = 'weblibre_profiles'; const profilesDirName = 'weblibre_profiles';
const profileDirPrefix = 'profile-'; const profileDirPrefix = 'profile-';
@@ -44,30 +42,15 @@ final profileTransformer =
}, },
); );
final profileMozillaDirectoryTransformer =
StreamTransformer<Directory, Directory>.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<List<Directory>> getAvailableProfileDirectories(Directory profilesDir) { Future<List<Directory>> getAvailableProfileDirectories(Directory profilesDir) {
return profilesDir.list().transform(profileTransformer).toList(); return profilesDir.list().transform(profileTransformer).toList();
} }
Future<void> clearMozillaProfileCache(String profileId) async { Future<void> clearMozillaProfileCache(
final cacheDir = await getApplicationCacheDirectory(); Directory profileDir,
String profileId,
) async {
final cacheDir = Directory(p.join(profileDir.path, 'cache'));
final mozillaCacheDir = Directory(p.join(cacheDir.path, profileId)); final mozillaCacheDir = Directory(p.join(cacheDir.path, profileId));
if (await mozillaCacheDir.exists()) { if (await mozillaCacheDir.exists()) {
@@ -76,9 +59,9 @@ Future<void> clearMozillaProfileCache(String profileId) async {
} }
/// Returns the list of Mozilla profile IDs (`.default` directory names) /// 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<String> getMozillaProfileIds(Directory profileDir) { List<String> getMozillaProfileIds(Directory profileDir) {
final mozillaDir = Directory(p.join(profileDir.path, 'mozilla')); final mozillaDir = Directory(p.join(profileDir.path, 'files', 'mozilla'));
if (!mozillaDir.existsSync()) return []; if (!mozillaDir.existsSync()) return [];
return mozillaDir return mozillaDir
@@ -89,25 +72,6 @@ List<String> getMozillaProfileIds(Directory profileDir) {
.toList(); .toList();
} }
Future<List<Directory>> 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<UuidValue?> readStartupProfile(Directory dir) async { Future<UuidValue?> readStartupProfile(Directory dir) async {
final file = File(p.join(dir.path, _startupProfileFileName)); final file = File(p.join(dir.path, _startupProfileFileName));