보관함 넣기: 넣다가 잠가 멈추면 넣지 못한 원본은 지우지 않음

- 예전부터 있던 버그: [원본 지우기 (이동)] 로 넣는 도중에 잠그면 넣기는 멈추지만, 넣지 못한 원본이 bad 에 들어가지 않아 그 원본까지 지워졌다 (데이터 잃음)
- putVaultAdd: 잠겨 멈출 때 남은 항목의 top 을 bad 에 넣는다 → 넣은 것만 원본을 지우고 나머지는 그대로
- 약속 시험 (실제 시험 폴더 · 실제 보관함): 원본 4개를 넣다가 3번째에서 잠그면 1 · 2 만 지워지고 3 · 4 는 바이트까지 그대로. 고치기 전 코드에서는 4.bin 이 지워져 실패함을 확인

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
split_vault
RyuJongJin 2026-10-22 20:07:38 +09:00
parent 88aadc7d3e
commit 43c8dab04c
2 changed files with 66 additions and 2 deletions

View File

@ -213,7 +213,7 @@ Future<({bool move, bool permanent})?> askVaultAdd(BuildContext context, List<St
/// 펼친 [plan] 을 [s] 의 보관함에 넣는다 (넣는 동안 잠그면 ([VaultUiState.vault] 가 null) 멈춤).
/// - 원격은 앱 전용 폴더에 받아 넣고 바로 지운다. 넣은 원본의 썸네일 캐시 ([thumbTemp] 아래) 도 지운다
/// - 출력: 넣은 수 [done] · 다 넣지 못한 고른 것 [bad] (plan.failed 포함). 오류는 [VaultUiState.snack] 으로 (던지지 않음)
/// - 출력: 넣은 수 [done] · 다 넣지 못한 고른 것 [bad] (plan.failed · 잠가서 멈춘 뒤 남은 것 포함). 오류는 [VaultUiState.snack] 으로 (던지지 않음)
Future<({int done, Set<String> bad})> putVaultAdd(VaultUiState s, List<String> paths, VaultAddPlan plan,
{required Future<String> Function() thumbTemp}) async {
s.setBusy(true);
@ -226,7 +226,13 @@ Future<({int done, Set<String> bad})> putVaultAdd(VaultUiState s, List<String> p
}
for (var i = 0; i < plan.files.length; i++) {
final x = plan.files[i];
if (s.vault == null) break; // 넣는 동안 잠갔으면 멈춤
if (s.vault == null) {
// 넣는 동안 잠갔으면 멈춤 - 넣지 못한 남은 것은 bad 로 (그 원본은 [원본 지우기 (이동)] 로 지우지 않게)
for (final rest in plan.files.skip(i)) {
bad.add(rest.top);
}
break;
}
s.setProgress('${i + 1} / ${plan.files.length} · ${vBasename(x.src)}');
String? fetched;
try {

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
@ -158,4 +159,61 @@ void main() {
await t.tap(find.text('나'));
expect(got, hasLength(2), reason: '바쁜 동안은 누르기가 꺼짐');
});
testWidgets('vault_session (넣기): [원본 지우기 (이동)] 로 넣다가 중간에 잠그면 넣은 것만 원본을 지우고, 넣지 못한 원본은 바이트까지 그대로', (t) async {
final src = Directory(p.join(tmp.path, 'src'));
final names = ['1.bin', '2.bin', '3.bin', '4.bin'];
final bytes = {for (final n in names) n: List<int>.generate(4096, (i) => (i * 7 + n.codeUnitAt(0)) % 256)};
final vault = await t.runAsync(() async {
src.createSync();
for (final n in names) {
File(p.join(src.path, n)).writeAsBytesSync(bytes[n]!);
}
Directory(p.join(tmp.path, 'thumbs')).createSync();
return Vault.create(Directory(p.join(tmp.path, 'vault')), 'pw-for-test', iterations: 1000);
});
final paths = [for (final n in names) p.join(src.path, n)];
late VaultUiState s;
// 화면의 잠그기와 같게: 열쇠를 버린다 (vault = null). "3 / 4" 를 보이는 때 = 1 · 2 를 넣은 뒤
s = VaultUiState(
changed: () {
if (s.progress?.startsWith('3 / ') ?? false) s.vault = null;
},
snack: (_) {})
..vault = vault;
final deleted = <String>[];
late BuildContext ctx;
await t.pumpWidget(MaterialApp(home: Builder(builder: (c) {
ctx = c;
return const Scaffold();
})));
var finished = false;
unawaited(vaultAddFlow(ctx, s, paths, thumbTemp: () async => p.join(tmp.path, 'thumbs'), deleteOriginals: (ok, {required permanent}) async {
deleted.addAll(ok);
for (final x in ok) {
File(x).deleteSync(); // 실제로 지운다 (탐색기의 지우기 대신)
}
}).whenComplete(() => finished = true));
Future<void> until(bool Function() ok) async {
for (var i = 0; i < 300 && !ok(); i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 20)));
await t.pump(const Duration(milliseconds: 20));
}
}
await until(() => find.text('원본 지우기 (이동)').evaluate().isNotEmpty);
await t.tap(find.text('원본 지우기 (이동)'));
await t.pump();
await t.tap(find.widgetWithText(FilledButton, '넣기'));
await until(() => finished);
expect(finished, isTrue);
expect(s.vault, isNull, reason: '중간에 잠금');
expect([for (final e in vault!.entries) e.name], unorderedEquals(['1.bin', '2.bin']), reason: '잠그기 전 둘만 들어감');
expect(deleted, unorderedEquals([paths[0], paths[1]]), reason: '넣은 것만 원본 지우기로');
for (final n in ['3.bin', '4.bin']) {
final f = File(p.join(src.path, n));
expect(f.existsSync(), isTrue, reason: '넣지 못한 원본 $n 은 남음');
expect(f.readAsBytesSync(), bytes[n], reason: '$n 바이트 그대로');
}
});
}