diff --git a/lib/common/task.dart b/lib/common/task.dart index dfe737b..bb737fd 100644 --- a/lib/common/task.dart +++ b/lib/common/task.dart @@ -641,23 +641,35 @@ Future restoreTask() async { ); } +Future restoreBackupArchive( + String backupFilePath, + String restoreDirPath, +) async { + final input = InputFileStream(backupFilePath); + try { + final archive = ZipDecoder().decodeStream(input); + final restoreDir = Directory(restoreDirPath); + await restoreDir.create(recursive: true); + for (final file in archive.files) { + final outPath = join(restoreDirPath, posix.normalize(file.name)); + final outputStream = OutputFileStream(outPath); + try { + file.writeContent(outputStream); + } finally { + await outputStream.close(); + } + } + } finally { + await input.close(); + } +} + Future _restoreTask(RootIsolateToken token) async { BackgroundIsolateBinaryMessenger.ensureInitialized(token); final backupFilePath = await appPath.backupFilePath; final restoreDirPath = await appPath.restoreDirPath; final homeDirPath = await appPath.homeDirPath; - final zipDecoder = ZipDecoder(); - final input = InputFileStream(backupFilePath); - final archive = zipDecoder.decodeStream(input); - final dir = Directory(restoreDirPath); - await dir.create(recursive: true); - for (final file in archive.files) { - final outPath = join(restoreDirPath, posix.normalize(file.name)); - final outputStream = OutputFileStream(outPath); - file.writeContent(outputStream); - await outputStream.close(); - } - await input.close(); + await restoreBackupArchive(backupFilePath, restoreDirPath); final restoreConfigFile = File(join(restoreDirPath, configJsonName)); if (!await restoreConfigFile.exists()) { throw currentAppLocalizations.invalidBackupFile; diff --git a/test/common/task_test.dart b/test/common/task_test.dart index a5549de..7101286 100644 --- a/test/common/task_test.dart +++ b/test/common/task_test.dart @@ -1,9 +1,11 @@ import 'dart:io'; +import 'package:archive/archive.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart' as p; void main() { group('legacy backup identifiers', () { @@ -21,6 +23,30 @@ void main() { }); }); + test('releases a backup file when extraction fails', () async { + final directory = await Directory.systemTemp.createTemp( + 'fl_clash_restore_lock_test_', + ); + addTearDown(() => directory.delete(recursive: true)); + final backupFile = File(p.join(directory.path, 'backup.zip')); + final restoreDir = Directory(p.join(directory.path, 'restore')); + await restoreDir.create(); + await Directory(p.join(restoreDir.path, configJsonName)).create(); + final archive = Archive() + ..addFile(ArchiveFile.string(configJsonName, '{}')); + final zipBytes = ZipEncoder().encodeBytes(archive); + await backupFile.writeAsBytes(zipBytes); + + await expectLater( + restoreBackupArchive(backupFile.path, restoreDir.path), + throwsA(isA()), + ); + + final result = await Process.run('lsof', ['-Fn', '-p', '$pid']); + expect(result.exitCode, 0); + expect(result.stdout, isNot(contains(backupFile.path))); + }); + test('profile ipv6 value wins over the client fallback', () { final result = applyCorePatchConfig( rawConfig: {'ipv6': true, 'ip-version': 'ipv6-prefer'},