From a8bc585b1a189c5b9826d648f9f65a0f36c0495b Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Tue, 20 Oct 2026 12:26:08 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=ED=99=94:=20=ED=99=95=EC=9D=B8=ED=95=9C=20=EC=8C=8D?= =?UTF-8?q?=EB=8F=84=20rsync=20=EC=98=B5=EC=85=98=EC=9D=B4=20=EB=B0=94?= =?UTF-8?q?=EB=80=8C=EC=96=B4=20=EC=A7=80=EC=9A=B8=20=EA=B2=83=EC=9D=B4=20?= =?UTF-8?q?=EB=8A=98=EB=A9=B4=20=EB=8B=A4=EC=8B=9C=20=EB=AC=BB=EA=B8=B0=20?= =?UTF-8?q?(=EA=B2=80=ED=86=A0=20=EB=8C=80=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 관리자 3a 판단 (10/20, 데이터 쪽이라 안전한 쪽으로): 로컬 · 원격 쌍 모두. - 쌍에 확인할 때의 rsync 옵션을 기억 (LiveSyncPair.confirmedOptions, 설정 파일에 저장 - 틀린 값은 없는 것으로). - 맞추기 전에 지금 옵션이 기억한 것과 다르면, 두 옵션으로 지울 것을 견주어 늘었으면 (--delete-excluded · --exclude · --filter 를 바꿔 새로 지워질 것이 생김) 지우지 않고 맞춘 뒤 다시 묻는다 (42 와 같은 창, 주황 "옵션이 바뀌어 지울 것이 늘었습니다" 줄). 늘지 않았으면 묻지 않고 지금 옵션을 기억. - 이 값이 생기기 전에 확인한 쌍은 예전 동작 (--delete-excluded 를 따르지 않음) 과 견준다 - 업데이트 뒤 --delete-excluded 로 새로 지워질 것이 있으면 묻고, 없으면 기억만 (처음 한 번 설정을 저장). - 확인 창 높이를 화면 높이에 따라 (360 ~ 520) - 412 세로에서 거른 것 묶음이 잘려 보이던 것 (캡처로 찾음). 시험: live_sync_scene_test (로컬, 실제 rsync - 늘지 않는 바꿈 · --delete-excluded 더함 · 거르기 뺌 · 예전 쌍 · 설정 저장), webdav_test (원격 쌍). LiveSync 를 쓰는 시험 14개 파일 148개 통과. 캡처 windows_live_excl_shot_test 를 다시 묻는 창으로 (ko · en · ja × 412 · 960, 넘침 0) → release\test\shots_live_excl. Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 1 + assets/l10n/ja.json | 1 + assets/l10n/zh-Hans.json | 1 + .../windows_live_excl_shot_test.dart | 4 +- lib/app/live_sync.dart | 46 ++++++++++- lib/app/settings.dart | 15 +++- lib/ui/monitor_page.dart | 9 ++- test/live_sync_scene_test.dart | 79 +++++++++++++++++++ test/webdav_test.dart | 29 +++++++ 9 files changed, 177 insertions(+), 8 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 5d69969..fb900f3 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -2427,6 +2427,7 @@ "지우기가 들어 있는 작업입니다": "This task deletes files", "지우기가 들어간 실행이라, 비교하지 못하면 실행할 수 없습니다. 원본 · 대상을 확인한 뒤 [다시 비교] 를 누르세요.": "This run deletes files, so it can't run without a comparison. Check the source and target, then press [Compare again].", "지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.": "This run deletes files, so you can run it after the list of deletions appears.", + "지우기를 확인한 뒤 rsync 옵션이 바뀌어 지울 것이 늘었습니다. 다시 확인해 주세요.": "The rsync options changed after you confirmed deleting, and more items would now be deleted. Please confirm again.", "지우는 폴더 안에 목록에서 숨긴 항목 {0}개가 있습니다 - 함께 지워집니다": "The folders being deleted contain {0} items hidden from the list - they will be deleted too", "지우면 휴지통으로": "Delete to the Recycle Bin", "지우지 못한 파일: {0}": "Couldn't delete: {0}", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 32be31e..040b7f6 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -2427,6 +2427,7 @@ "지우기가 들어 있는 작업입니다": "削除を含む作業です", "지우기가 들어간 실행이라, 비교하지 못하면 실행할 수 없습니다. 원본 · 대상을 확인한 뒤 [다시 비교] 를 누르세요.": "削除を含む実行のため、比較できないと実行できません。元・対象を確認してから [もう一度比較] を押してください。", "지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.": "削除を含む実行のため、削除一覧が表示されてから実行できます。", + "지우기를 확인한 뒤 rsync 옵션이 바뀌어 지울 것이 늘었습니다. 다시 확인해 주세요.": "削除を確認した後に rsync オプションが変わり、削除するものが増えました。もう一度確認してください。", "지우는 폴더 안에 목록에서 숨긴 항목 {0}개가 있습니다 - 함께 지워집니다": "削除するフォルダーの中にリストから隠した項目が {0} 件あります - 一緒に削除されます", "지우면 휴지통으로": "削除するとごみ箱へ", "지우지 못한 파일: {0}": "削除できなかったファイル: {0}", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index cfb9940..33c700b 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -2427,6 +2427,7 @@ "지우기가 들어 있는 작업입니다": "此任务包含删除", "지우기가 들어간 실행이라, 비교하지 못하면 실행할 수 없습니다. 원본 · 대상을 확인한 뒤 [다시 비교] 를 누르세요.": "此操作包含删除,无法比较时不能执行。请检查源和目标后按 [重新比较]。", "지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.": "此操作包含删除,需在显示删除列表后才能执行。", + "지우기를 확인한 뒤 rsync 옵션이 바뀌어 지울 것이 늘었습니다. 다시 확인해 주세요.": "确认删除后 rsync 选项已更改,要删除的项目增加了。请再次确认。", "지우는 폴더 안에 목록에서 숨긴 항목 {0}개가 있습니다 - 함께 지워집니다": "要删除的文件夹中有 {0} 个从列表中隐藏的项 - 将一并删除", "지우면 휴지통으로": "删除时移到回收站", "지우지 못한 파일: {0}": "无法删除的文件: {0}", diff --git a/integration_test/windows_live_excl_shot_test.dart b/integration_test/windows_live_excl_shot_test.dart index a97af62..36f93d9 100644 --- a/integration_test/windows_live_excl_shot_test.dart +++ b/integration_test/windows_live_excl_shot_test.dart @@ -54,12 +54,14 @@ void main() { ..rsyncSource = 'custom' ..rsyncPath = rsync ..rsyncOptions = '-av --exclude=*.tmp --delete-excluded'; - final pair = LiveSyncPair(src, dst, method: 'rsync', delete: true); + // 이미 --exclude 만으로 확인한 쌍에 --delete-excluded 를 더함 → 다시 묻는 창 (주황 이유 줄 · 거른 것 묶음) + final pair = LiveSyncPair(src, dst, method: 'rsync', delete: true, deleteConfirmed: true, confirmedOptions: '-av --exclude=*.tmp'); c.settings.liveSyncPairs = [pair]; final live = LiveSync(c); addTearDown(live.dispose); await t.runAsync(() => live.syncNow(pair)); expect(live.toDeleteExcluded[LiveSync.keyOf(pair)], hasLength(3)); + expect(live.reasked, contains(LiveSync.keyOf(pair))); final navKey = GlobalKey(); final shotKey = GlobalKey(); await t.pumpWidget(RepaintBoundary( diff --git a/lib/app/live_sync.dart b/lib/app/live_sync.dart index d621450..b833ece 100644 --- a/lib/app/live_sync.dart +++ b/lib/app/live_sync.dart @@ -154,6 +154,30 @@ class LiveSync extends ChangeNotifier { /// 로컬 rsync 쌍은 rsync 가 직접 따르므로 대기 목록 (diff) 에도 같이 RsyncFilter _filterOf(LiveSyncPair x) => x.method == 'rsync' ? RsyncFilter.parse(c.settings.rsyncOptions) : RsyncFilter.none; + /// 지울 것에 영향을 주는 옵션 (rsync 쌍만 - 거르기 · --delete-excluded). 확인할 때 쌍에 기억한다 ([LiveSyncPair.confirmedOptions]) + String _optionsOf(LiveSyncPair x) => x.method == 'rsync' ? c.settings.rsyncOptions : ''; + + /// 관리자 3a 판단 (10/20): 확인한 쌍이라도 옵션이 바뀌어 지울 것이 늘었으면 다시 묻는다 (로컬 · 원격 쌍 모두). + /// 늘지 않았으면 (줄거나 같음) 묻지 않고 지금 옵션을 기억한다. 예전에 확인한 쌍 (기억한 옵션 없음) 은 예전 동작 + /// (--delete-excluded 를 따르지 않음) 과 견준다 - 업데이트 뒤 --delete-excluded 로 새로 지워질 것이 있으면 묻는다 + Future _deletesGrew(LiveSyncPair x) async { + final old = x.confirmedOptions; + final now = _filterOf(x); + final before = old == null + ? RsyncFilter(now.rules, unsupported: now.unsupported) + : x.method == 'rsync' + ? RsyncFilter.parse(old) + : RsyncFilter.none; + List gone(List d) => [for (final s in d) if (s.startsWith('− ')) s.substring(2)]; + final a = gone(await diff(x, limit: 100000, filter: now)); + if (a.isEmpty) return false; + final b = gone(await diff(x, limit: 100000, filter: before)); + return a.any((d) => !b.any((e) => d == e || d.startsWith('$e/'))); + } + + /// 확인한 뒤 옵션이 바뀌어 다시 묻는 쌍 (확인 창에 그 이유를 보인다) + final reasked = {}; + /// 받는 쪽에만 있는 [path] (상대 경로 [r]) 가 지워지는 모양을 [out] 에 "− 경로" 로 (rsync · _mirrorV 와 같게): /// 거른 것은 지우지 않고 (--delete-excluded 면 지움), 안에 거른 것이 든 폴더는 남기고 그 안의 거르지 않은 것만. /// 10/20 (15, 28 의 장면으로 찾음): 예전에는 그런 폴더도 "− 폴더" 한 줄이라 지울지 묻는 창에 폴더가 통째로 보이고, @@ -375,9 +399,10 @@ class LiveSync extends ChangeNotifier { /// 지울 목록을 보고 결정: [delete] true = 지우기 포함으로 맞추기 (확인함), false = 지우기 끄기. 그 뒤 바로 맞춘다 Future decideDelete(LiveSyncPair x, {required bool delete}) async { - final now = delete ? x.copyWith(deleteConfirmed: true) : x.copyWith(delete: false); + final now = delete ? x.copyWith(deleteConfirmed: true, confirmedOptions: _optionsOf(x)) : x.copyWith(delete: false); toDelete.remove(keyOf(x)); toDeleteExcluded.remove(keyOf(x)); + reasked.remove(keyOf(x)); await _setPair(x, now); await syncNow(now); } @@ -478,13 +503,27 @@ class LiveSync extends ChangeNotifier { final asked = x; try { // 42: 지우기를 아직 확인하지 않았으면, 지울 것이 있을 때 지우지 않고 맞추고 확인을 기다린다 (목록은 [toDelete]) - if (x.delete && !x.deleteConfirmed) { + var ask = x.delete && !x.deleteConfirmed; + if (x.delete && x.deleteConfirmed && x.confirmedOptions != _optionsOf(x)) { + if (await _deletesGrew(x)) { + ask = true; + reasked.add(k); + } else { + final now = x.copyWith(confirmedOptions: _optionsOf(x)); + await _setPair(x, now); + x = now; + } + } + if (ask) { final excl = []; final del = [ for (final d in await diff(x, limit: 100000, filter: _filterOf(x), excluded: excl)) if (d.startsWith('− ')) d.substring(2) ]; if (del.isEmpty) { - await _setPair(x, x.copyWith(deleteConfirmed: true)); + final now = x.copyWith(deleteConfirmed: true, confirmedOptions: _optionsOf(x)); + await _setPair(x, now); + x = now; + reasked.remove(k); } else { toDelete[k] = del; toDeleteExcluded[k] = excl; @@ -493,6 +532,7 @@ class LiveSync extends ChangeNotifier { } else { toDelete.remove(k); toDeleteExcluded.remove(k); + reasked.remove(k); } // 지우기 포함이면 원본을 확실히 읽을 수 있을 때만 (rsync --delete · robocopy /PURGE 도). 못 읽으면 멈추고 알린다 final allowEmpty = _allowEmpty.remove(k); diff --git a/lib/app/settings.dart b/lib/app/settings.dart index e882b00..50368f3 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -79,21 +79,28 @@ class LiveSyncPair { /// "지우기" 를 사용자가 지울 목록을 보고 확인했는지. 확인 전에는 지울 것이 있으면 지우지 않고 맞추며 확인을 기다린다 (42). /// 이 값이 생기기 전부터 돌던 쌍은 확인한 것으로 본다. final bool deleteConfirmed; + + /// 지우기를 확인할 때의 rsync 옵션 (rsync 쌍만, 그 밖은 ''). 확인한 뒤 옵션이 바뀌어 지울 것이 늘면 다시 묻는다 + /// (관리자 3a 판단 10/20 - --delete-excluded · --exclude · --filter 등). null = 이 값이 생기기 전에 확인한 쌍 + final String? confirmedOptions; const LiveSyncPair(this.source, this.target, {this.method = 'builtin', this.delete = false, this.enabled = true, this.schedule = const [], - this.deleteConfirmed = false}); + this.deleteConfirmed = false, + this.confirmedOptions}); /// [delete] 를 새로 켜면 확인은 다시 받는다 - LiveSyncPair copyWith({String? method, bool? delete, bool? enabled, List? schedule, bool? deleteConfirmed}) => + LiveSyncPair copyWith( + {String? method, bool? delete, bool? enabled, List? schedule, bool? deleteConfirmed, String? confirmedOptions}) => LiveSyncPair(source, target, method: method ?? this.method, delete: delete ?? this.delete, enabled: enabled ?? this.enabled, schedule: schedule ?? this.schedule, - deleteConfirmed: deleteConfirmed ?? (delete == true && !this.delete ? false : this.deleteConfirmed)); + deleteConfirmed: deleteConfirmed ?? (delete == true && !this.delete ? false : this.deleteConfirmed), + confirmedOptions: confirmedOptions ?? this.confirmedOptions); Map toJson() => { 'source': source, @@ -103,6 +110,7 @@ class LiveSyncPair { 'enabled': enabled, 'schedule': schedule, 'deleteConfirmed': deleteConfirmed, + if (confirmedOptions != null) 'confirmedOptions': confirmedOptions, }; factory LiveSyncPair.fromJson(Map j) => LiveSyncPair( @@ -114,6 +122,7 @@ class LiveSyncPair { schedule: [for (final x in (j['schedule'] as List?) ?? const []) '$x'], // 이 값이 없는 예전 쌍은 이미 지우기 포함으로 돌던 것 → 확인한 것으로 deleteConfirmed: j['deleteConfirmed'] as bool? ?? true, + confirmedOptions: j['confirmedOptions'] is String ? j['confirmedOptions'] as String : null, ); } diff --git a/lib/ui/monitor_page.dart b/lib/ui/monitor_page.dart index 193f465..5795994 100644 --- a/lib/ui/monitor_page.dart +++ b/lib/ui/monitor_page.dart @@ -90,8 +90,15 @@ Future confirmLiveDeletes(BuildContext context, LiveSync live, LiveSyncPai title: Text(trf('대상에서 {0}개를 지울까요?', [list.length])), content: SizedBox( width: 560, - height: 360, + // 세로 폰에서도 목록 (특히 거른 것 묶음) 이 더 보이게 화면 높이를 따라 (360 ~ 520) + height: (MediaQuery.sizeOf(ctx).height * 0.55).clamp(360.0, 520.0), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + // 관리자 3a 판단 (10/20): 확인한 쌍인데 옵션이 바뀌어 지울 것이 늘어 다시 묻는 것 + if (live.reasked.contains(LiveSync.keyOf(x))) ...[ + Text(tr('지우기를 확인한 뒤 rsync 옵션이 바뀌어 지울 것이 늘었습니다. 다시 확인해 주세요.'), + style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.orangeAccent)), + const SizedBox(height: 6), + ], Text(trf('"{0}" 에는 없고 대상 "{1}" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).', [vDisplay(x.source), vDisplay(x.target)])), const SizedBox(height: 8), diff --git a/test/live_sync_scene_test.dart b/test/live_sync_scene_test.dart index 7d2d8c0..dd66557 100644 --- a/test/live_sync_scene_test.dart +++ b/test/live_sync_scene_test.dart @@ -114,4 +114,83 @@ void main() { await tester.tap(find.text('나중에')); await tester.pump(); }); + + test('관리자 3a 판단 (10/20): 확인한 쌍도 rsync 옵션이 바뀌어 지울 것이 늘면 다시 묻고, 늘지 않으면 묻지 않고 기억', () async { + final rsync = sceneRsync; + if (rsync == null) return markTestSkipped('앱에 든 rsync 가 없음'); + final tmp = Directory.systemTemp.createTempSync('jj_reask_'); + addTearDown(() => tmp.deleteSync(recursive: true)); + final src = p.join(tmp.path, 's'), dst = p.join(tmp.path, 'd'); + for (final (dir, rel) in [(src, 'a.txt'), (dst, 'only.tmp'), (dst, 'keep/x.tmp')]) { + File(p.join(dir, rel)) + ..createSync(recursive: true) + ..writeAsStringSync(rel); + } + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + c.settings + ..rsyncSource = 'custom' + ..rsyncPath = rsync + ..rsyncOptions = '-av --exclude=*.tmp'; + // 이미 이 옵션으로 확인한 쌍 + c.settings.liveSyncPairs = [ + LiveSyncPair(src, dst, method: 'rsync', delete: true, deleteConfirmed: true, confirmedOptions: '-av --exclude=*.tmp'), + ]; + final live = LiveSync(c); + addTearDown(live.dispose); + LiveSyncPair pair() => c.settings.liveSyncPairs.single; + final k = LiveSync.keyOf(pair()); + await live.syncNow(pair()); + expect(live.toDelete[k], isNull); + expect(File(p.join(dst, 'a.txt')).existsSync(), isTrue); + + // ① 지울 것이 늘지 않는 바꿈 (-h 더하기) → 묻지 않고 지금 옵션을 기억 + c.settings.rsyncOptions = '-avh --exclude=*.tmp'; + await live.syncNow(pair()); + expect(live.toDelete[k], isNull); + expect(pair().confirmedOptions, '-avh --exclude=*.tmp'); + + // ② --delete-excluded 를 더함 → 거른 것이 새로 지워지므로 다시 묻고, 확인 전에는 지우지 않음 + c.settings.rsyncOptions = '-avh --exclude=*.tmp --delete-excluded'; + await live.syncNow(pair()); + expect(live.toDelete[k], unorderedEquals(['only.tmp', 'keep'])); + expect(live.reasked, contains(k)); + expect(live.status[k]!.$2, contains('확인 필요')); + expect(File(p.join(dst, 'only.tmp')).existsSync(), isTrue, reason: '확인 전에는 지우지 않음'); + expect(pair().deleteConfirmed, isTrue, reason: '쌍의 확인은 그대로 - 바뀐 옵션만 다시 묻는다'); + // 확인하면 지우고 그 옵션을 기억 + await live.decideDelete(pair(), delete: true); + expect(File(p.join(dst, 'only.tmp')).existsSync(), isFalse); + expect(Directory(p.join(dst, 'keep')).existsSync(), isFalse); + expect(pair().confirmedOptions, '-avh --exclude=*.tmp --delete-excluded'); + expect(live.reasked, isNot(contains(k))); + + // ③ 거르기를 빼면 (--exclude 를 지움) 지울 것은 줄거나 같다 → 묻지 않음 + c.settings.rsyncOptions = '-avh'; + await live.syncNow(pair()); + expect(live.toDelete[k], isNull); + expect(pair().confirmedOptions, '-avh'); + + // ④ 예전에 확인한 쌍 (기억한 옵션 없음): --delete-excluded 로 새로 지워질 것이 있으면 묻는다 · 없으면 기억만 + File(p.join(dst, 'new.tmp')).writeAsStringSync('n'); + c.settings + ..rsyncOptions = '-av --exclude=*.tmp --delete-excluded' + ..liveSyncPairs = [LiveSyncPair.fromJson({'source': src, 'target': dst, 'method': 'rsync', 'delete': true})]; + expect(pair().confirmedOptions, isNull); + await live.syncNow(pair()); + expect(live.toDelete[k], ['new.tmp']); + expect(File(p.join(dst, 'new.tmp')).existsSync(), isTrue); + c.settings + ..rsyncOptions = '-av --exclude=*.tmp' + ..liveSyncPairs = [LiveSyncPair.fromJson({'source': src, 'target': dst, 'method': 'rsync', 'delete': true})]; + await live.syncNow(pair()); + expect(live.toDelete[k], isNull); + expect(pair().confirmedOptions, '-av --exclude=*.tmp'); + expect(File(p.join(dst, 'new.tmp')).existsSync(), isTrue, reason: '거른 것 - 지우지 않음'); + + // 설정 파일: 기억한 옵션이 저장 · 읽기에서 남는다 + final j = pair().toJson(); + expect(j['confirmedOptions'], '-av --exclude=*.tmp'); + expect(LiveSyncPair.fromJson(j).confirmedOptions, '-av --exclude=*.tmp'); + expect(LiveSyncPair.fromJson({...j, 'confirmedOptions': 3}).confirmedOptions, isNull, reason: '틀린 값은 없는 것으로'); + }); } diff --git a/test/webdav_test.dart b/test/webdav_test.dart index be820b0..6db552e 100644 --- a/test/webdav_test.dart +++ b/test/webdav_test.dart @@ -152,6 +152,8 @@ void main() { lf('live/sub/b.txt', 'bb'); rf('mirror/old.txt', 'o'); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + // 예전에 확인한 쌍은 처음 맞출 때 지금 옵션을 기억하며 설정을 저장한다 (서버 목록도 설정에서 다시 읽음) - 시험 서버를 설정에도 + c.settings.webdavServers = [DavServer(id: id, name: '집 NAS', url: server.url, user: 'user', password: 'pass')]; final pair = LiveSyncPair(p.join(local.path, 'live'), 'dav://$id/mirror', method: 'rsync', delete: true, deleteConfirmed: true); c.settings.liveSyncPairs = [pair]; final live = LiveSync(c); @@ -359,4 +361,31 @@ void main() { } expect(bad, isEmpty, reason: bad.join('\n')); }, timeout: const Timeout(Duration(minutes: 5))); + + test('관리자 3a 판단 (10/20): 원격 (WebDAV) 쌍도 확인한 뒤 옵션이 바뀌어 지울 것이 늘면 다시 묻는다', () async { + lf('rq/a.txt', 'a'); + rf('rqd/only.tmp', 'o'); + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + c.settings + ..webdavServers = [DavServer(id: id, name: '집 NAS', url: server.url, user: 'user', password: 'pass')] + ..rsyncOptions = '-av --exclude=*.tmp' + ..liveSyncPairs = [ + LiveSyncPair(p.join(local.path, 'rq'), 'dav://$id/rqd', + method: 'rsync', delete: true, deleteConfirmed: true, confirmedOptions: '-av --exclude=*.tmp'), + ]; + final live = LiveSync(c); + addTearDown(live.dispose); + LiveSyncPair pair() => c.settings.liveSyncPairs.single; + final k = LiveSync.keyOf(pair()); + await live.syncNow(pair()); + expect(live.toDelete[k], isNull); + c.settings.rsyncOptions = '-av --exclude=*.tmp --delete-excluded'; + await live.syncNow(pair()); + expect(live.toDelete[k], ['only.tmp']); + expect(live.reasked, contains(k)); + expect(File(p.join(remote.path, 'rqd', 'only.tmp')).existsSync(), isTrue, reason: '확인 전에는 지우지 않음'); + await live.decideDelete(pair(), delete: true); + expect(File(p.join(remote.path, 'rqd', 'only.tmp')).existsSync(), isFalse); + expect(pair().confirmedOptions, '-av --exclude=*.tmp --delete-excluded'); + }); }