migrate mozilla caches into profiles
This commit is contained in:
@@ -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(
|
||||||
return duplicates
|
p.join(selectedProfileDir.path, 'files', 'mozilla'),
|
||||||
.firstWhereOrNull((dir) => p.isWithin(profileDir, dir))
|
|
||||||
.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) {
|
final oldType = await FileSystemEntity.type(
|
||||||
await Directory(mozillaPath).rename(backupPath);
|
oldDir.path,
|
||||||
} 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();
|
Future<void> _migrateGeckoCache() async {
|
||||||
case FileSystemEntityType.directory:
|
final profileIds = fs.getMozillaProfileIds(selectedProfileDir);
|
||||||
case FileSystemEntityType.file:
|
final globalCacheDir = Directory(p.join(dataDir.path, 'cache'));
|
||||||
case FileSystemEntityType.unixDomainSock:
|
|
||||||
case FileSystemEntityType.pipe:
|
|
||||||
default:
|
|
||||||
await moveAside(currentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
for (final profileId in profileIds) {
|
||||||
await createLink();
|
final oldCache = Directory(p.join(globalCacheDir.path, profileId));
|
||||||
} on PathExistsException {
|
final newCache = Directory(
|
||||||
final retryType = await FileSystemEntity.type(
|
p.join(selectedProfileDir.path, 'cache', profileId),
|
||||||
mozillaPath,
|
|
||||||
followLinks: false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
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) {
|
|
||||||
final clearCache = useState(false);
|
|
||||||
|
|
||||||
return AlertDialog(
|
|
||||||
icon: const Icon(Icons.warning),
|
icon: const Icon(Icons.warning),
|
||||||
title: const Text('Switch User'),
|
title: const Text('Switch User'),
|
||||||
content: Column(
|
content: Text(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Switching to User '$profileName' will require a restart of the Browser.",
|
"Switching to User '$profileName' will require a restart of the Browser.",
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
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: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pop((shouldSwitch: false, clearCache: false));
|
context.pop(false);
|
||||||
},
|
},
|
||||||
child: const Text('Cancel'),
|
child: const Text('Cancel'),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pop((shouldSwitch: true, clearCache: clearCache.value));
|
context.pop(true);
|
||||||
},
|
},
|
||||||
child: const Text('Switch Profile'),
|
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();
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user