Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions lib/core/storage/app_data_root.dart
Original file line number Diff line number Diff line change
Expand Up @@ -194,19 +194,36 @@ abstract final class AppDataRoot {
return const [];
}

/// Copies [from] into [to] so that `querya_desktop/querya.db` appears last
/// and whole: the migration is skipped once that file exists, so an
/// interrupted copy must never leave it behind. Its `-wal` / `-shm` files
/// are copied before it, and the database goes in under a temporary name
/// and is renamed into place. [from] is not touched.
static Future<void> _copyDirectory(Directory from, Directory to) async {
if (!await to.exists()) {
await to.create(recursive: true);
}
final dbRelative = p.join('querya_desktop', 'querya.db');
File? legacyDb;
await for (final entity in from.list(recursive: true, followLinks: false)) {
final relative = p.relative(entity.path, from: from.path);
final destPath = p.join(to.path, relative);
if (entity is Directory) {
await Directory(destPath).create(recursive: true);
} else if (entity is File) {
if (relative == dbRelative) {
legacyDb = entity;
continue;
}
await File(destPath).parent.create(recursive: true);
await entity.copy(destPath);
}
}
if (legacyDb == null) return;
final destDb = p.join(to.path, dbRelative);
await Directory(p.dirname(destDb)).create(recursive: true);
final staged = '$destDb.migrating';
await legacyDb.copy(staged);
await File(staged).rename(destDb);
}
}
55 changes: 55 additions & 0 deletions test/core/storage/app_data_root_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -159,5 +159,60 @@ void main() {
);
expect(again, isFalse);
});

test('a copy that fails midway leaves no querya.db and is retried (#1766)',
() async {
final legacy = Directory(p.join(tempDir.path, 'legacy_support'));
final next = Directory(p.join(tempDir.path, 'new_support'));
await Directory(p.join(legacy.path, 'querya_desktop'))
.create(recursive: true);
await File(p.join(legacy.path, 'querya_desktop', 'querya.db'))
.writeAsString('legacy-db');
await File(p.join(legacy.path, 'querya_desktop', 'querya.db-wal'))
.writeAsString('legacy-wal');
await Directory(p.join(legacy.path, 'themes')).create(recursive: true);
await File(p.join(legacy.path, 'themes', 'a.json')).writeAsString('{}');
// A directory where a file has to go makes that copy fail.
await Directory(p.join(next.path, 'themes', 'a.json'))
.create(recursive: true);

await expectLater(
AppDataRoot.migrateLegacySupportIfNeeded(
newSupport: next,
legacyCandidates: [legacy],
),
throwsA(isA<FileSystemException>()),
);
expect(
await File(p.join(next.path, 'querya_desktop', 'querya.db')).exists(),
isFalse,
);

await Directory(p.join(next.path, 'themes', 'a.json')).delete();
final retried = await AppDataRoot.migrateLegacySupportIfNeeded(
newSupport: next,
legacyCandidates: [legacy],
);
expect(retried, isTrue);
expect(
await File(p.join(next.path, 'querya_desktop', 'querya.db'))
.readAsString(),
'legacy-db',
);
expect(
await File(p.join(next.path, 'querya_desktop', 'querya.db-wal'))
.readAsString(),
'legacy-wal',
);
expect(
await File(p.join(next.path, 'querya_desktop', 'querya.db.migrating'))
.exists(),
isFalse,
);
expect(
await File(p.join(legacy.path, 'querya_desktop', 'querya.db')).exists(),
isTrue,
);
});
});
}
Loading