exclude cache

This commit is contained in:
Fabian Freund
2026-03-23 13:57:08 +01:00
parent 296226760f
commit 053d4861df
2 changed files with 117 additions and 1 deletions
@@ -43,6 +43,7 @@ class ProfileBackupScreen extends HookConsumerWidget {
final passwordController = useMemoized(() => FancyPasswordController());
final integrityVerification = useState(true);
final skipCaches = useState(false);
final skipPasswordConfirmation = useState(false);
final backupFuture = useState<Future<bool>?>(null);
@@ -111,6 +112,19 @@ class ProfileBackupScreen extends HookConsumerWidget {
'Automatically check that backups are complete and restorable',
),
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
value: skipCaches.value,
onChanged: disableInteraction
? null
: (value) {
skipCaches.value = value;
},
title: const Text('Skip Cache Directories'),
subtitle: const Text(
'Leave out temporary browser caches like page, icon, and thumbnail data to keep backups smaller',
),
),
ExpansionTile(
enabled: !disableInteraction,
childrenPadding: EdgeInsets.zero,
@@ -175,6 +189,7 @@ class ProfileBackupScreen extends HookConsumerWidget {
profile,
password: passwordTextController.text,
integrityCheck: integrityVerification.value,
skipCaches: skipCaches.value,
);
}
},
@@ -38,10 +38,89 @@ part 'user_backup.g.dart';
@Riverpod(keepAlive: true)
class UserBackupService extends _$UserBackupService {
static final dateFormatter = FixedDateTimeFormatter('YYYY-MM-DD_hhmmss');
static const _excludedBackupRelativePaths = {'cache'};
static final _safUtil = SafUtil();
static final _safStream = SafStream();
bool _isExcludedBackupPath(String relativePath) {
final normalizedPath = p.normalize(relativePath);
for (final excludedPath in _excludedBackupRelativePaths) {
if (normalizedPath == excludedPath ||
p.isWithin(excludedPath, normalizedPath)) {
return true;
}
}
return false;
}
Future<void> _copyCuratedBackupSource(
Directory rootDirectory,
Directory sourceDirectory,
Directory targetDirectory,
) async {
await targetDirectory.create(recursive: true);
await for (final entity in sourceDirectory.list(followLinks: false)) {
final relativePath = p.relative(entity.path, from: rootDirectory.path);
if (_isExcludedBackupPath(relativePath)) {
continue;
}
final targetPath = p.join(targetDirectory.path, p.basename(entity.path));
if (entity is Directory) {
await _copyCuratedBackupSource(
rootDirectory,
entity,
Directory(targetPath),
);
} else if (entity is File) {
await entity.copy(targetPath);
} else if (entity is Link) {
await Link(targetPath).create(await entity.target());
}
}
}
Future<Directory> _prepareBackupSourceDirectory(
Directory sourceDirectory, {
required bool skipCaches,
}) async {
if (!skipCaches) {
return sourceDirectory;
}
final tempDirectory = await getTemporaryDirectory();
final curatedDirectory = Directory(
p.join(
tempDirectory.path,
'backup_source_${DateTime.now().microsecondsSinceEpoch}',
),
);
try {
await _copyCuratedBackupSource(
sourceDirectory,
sourceDirectory,
curatedDirectory,
);
return curatedDirectory;
} catch (_) {
try {
if (await curatedDirectory.exists()) {
await curatedDirectory.delete(recursive: true);
}
} catch (_) {
// Ignore cleanup errors for partially copied backup sources.
}
rethrow;
}
}
Uri _requireBackupDirectoryUri() {
final uri = ref.read(backupDirectoryUriProvider);
if (uri == null) {
@@ -61,18 +140,26 @@ class UserBackupService extends _$UserBackupService {
Profile profile, {
required String password,
required bool integrityCheck,
required bool skipCaches,
}) async {
final dirUri = _requireBackupDirectoryUri();
final timestamp = dateFormatter.encode(DateTime.now());
final fileName = 'backup_${profile.name}_$timestamp.weblibre';
final sourceDirectory = filesystem.getProfileDir(profile.uuidValue);
final tempDir = await getTemporaryDirectory();
final tempFile = File(p.join(tempDir.path, fileName));
Directory? curatedSourceDirectory;
try {
curatedSourceDirectory = await _prepareBackupSourceDirectory(
sourceDirectory,
skipCaches: skipCaches,
);
final backup = SecureArchivePack(
outputFile: tempFile,
sourceDirectory: filesystem.getProfileDir(profile.uuidValue),
sourceDirectory: curatedSourceDirectory,
argon2Params: Argon2Params.memoryConstrained(),
);
@@ -98,6 +185,20 @@ class UserBackupService extends _$UserBackupService {
stackTrace: s,
);
}
if (curatedSourceDirectory != null &&
curatedSourceDirectory.path != sourceDirectory.path) {
try {
if (await curatedSourceDirectory.exists()) {
await curatedSourceDirectory.delete(recursive: true);
}
} catch (e, s) {
logger.w(
'Failed to cleanup curated backup directory: ${curatedSourceDirectory.path}',
error: e,
stackTrace: s,
);
}
}
}
}