next8: + a8bc585 (실시간 동기화: 옵션이 바뀌어 지울 것이 늘면 다시 묻기) - 시험판 980
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>next8
commit
6e646b60c1
|
|
@ -2434,6 +2434,7 @@
|
||||||
"지우기가 들어 있는 작업입니다": "This task deletes files",
|
"지우기가 들어 있는 작업입니다": "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 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.",
|
"지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.": "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",
|
"지우는 폴더 안에 목록에서 숨긴 항목 {0}개가 있습니다 - 함께 지워집니다": "The folders being deleted contain {0} items hidden from the list - they will be deleted too",
|
||||||
"지우면 휴지통으로": "Delete to the Recycle Bin",
|
"지우면 휴지통으로": "Delete to the Recycle Bin",
|
||||||
"지우지 못한 파일: {0}": "Couldn't delete: {0}",
|
"지우지 못한 파일: {0}": "Couldn't delete: {0}",
|
||||||
|
|
|
||||||
|
|
@ -2434,6 +2434,7 @@
|
||||||
"지우기가 들어 있는 작업입니다": "削除を含む作業です",
|
"지우기가 들어 있는 작업입니다": "削除を含む作業です",
|
||||||
"지우기가 들어간 실행이라, 비교하지 못하면 실행할 수 없습니다. 원본 · 대상을 확인한 뒤 [다시 비교] 를 누르세요.": "削除を含む実行のため、比較できないと実行できません。元・対象を確認してから [もう一度比較] を押してください。",
|
"지우기가 들어간 실행이라, 비교하지 못하면 실행할 수 없습니다. 원본 · 대상을 확인한 뒤 [다시 비교] 를 누르세요.": "削除を含む実行のため、比較できないと実行できません。元・対象を確認してから [もう一度比較] を押してください。",
|
||||||
"지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.": "削除を含む実行のため、削除一覧が表示されてから実行できます。",
|
"지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.": "削除を含む実行のため、削除一覧が表示されてから実行できます。",
|
||||||
|
"지우기를 확인한 뒤 rsync 옵션이 바뀌어 지울 것이 늘었습니다. 다시 확인해 주세요.": "削除を確認した後に rsync オプションが変わり、削除するものが増えました。もう一度確認してください。",
|
||||||
"지우는 폴더 안에 목록에서 숨긴 항목 {0}개가 있습니다 - 함께 지워집니다": "削除するフォルダーの中にリストから隠した項目が {0} 件あります - 一緒に削除されます",
|
"지우는 폴더 안에 목록에서 숨긴 항목 {0}개가 있습니다 - 함께 지워집니다": "削除するフォルダーの中にリストから隠した項目が {0} 件あります - 一緒に削除されます",
|
||||||
"지우면 휴지통으로": "削除するとごみ箱へ",
|
"지우면 휴지통으로": "削除するとごみ箱へ",
|
||||||
"지우지 못한 파일: {0}": "削除できなかったファイル: {0}",
|
"지우지 못한 파일: {0}": "削除できなかったファイル: {0}",
|
||||||
|
|
|
||||||
|
|
@ -2434,6 +2434,7 @@
|
||||||
"지우기가 들어 있는 작업입니다": "此任务包含删除",
|
"지우기가 들어 있는 작업입니다": "此任务包含删除",
|
||||||
"지우기가 들어간 실행이라, 비교하지 못하면 실행할 수 없습니다. 원본 · 대상을 확인한 뒤 [다시 비교] 를 누르세요.": "此操作包含删除,无法比较时不能执行。请检查源和目标后按 [重新比较]。",
|
"지우기가 들어간 실행이라, 비교하지 못하면 실행할 수 없습니다. 원본 · 대상을 확인한 뒤 [다시 비교] 를 누르세요.": "此操作包含删除,无法比较时不能执行。请检查源和目标后按 [重新比较]。",
|
||||||
"지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.": "此操作包含删除,需在显示删除列表后才能执行。",
|
"지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.": "此操作包含删除,需在显示删除列表后才能执行。",
|
||||||
|
"지우기를 확인한 뒤 rsync 옵션이 바뀌어 지울 것이 늘었습니다. 다시 확인해 주세요.": "确认删除后 rsync 选项已更改,要删除的项目增加了。请再次确认。",
|
||||||
"지우는 폴더 안에 목록에서 숨긴 항목 {0}개가 있습니다 - 함께 지워집니다": "要删除的文件夹中有 {0} 个从列表中隐藏的项 - 将一并删除",
|
"지우는 폴더 안에 목록에서 숨긴 항목 {0}개가 있습니다 - 함께 지워집니다": "要删除的文件夹中有 {0} 个从列表中隐藏的项 - 将一并删除",
|
||||||
"지우면 휴지통으로": "删除时移到回收站",
|
"지우면 휴지통으로": "删除时移到回收站",
|
||||||
"지우지 못한 파일: {0}": "无法删除的文件: {0}",
|
"지우지 못한 파일: {0}": "无法删除的文件: {0}",
|
||||||
|
|
|
||||||
|
|
@ -54,12 +54,14 @@ void main() {
|
||||||
..rsyncSource = 'custom'
|
..rsyncSource = 'custom'
|
||||||
..rsyncPath = rsync
|
..rsyncPath = rsync
|
||||||
..rsyncOptions = '-av --exclude=*.tmp --delete-excluded';
|
..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];
|
c.settings.liveSyncPairs = [pair];
|
||||||
final live = LiveSync(c);
|
final live = LiveSync(c);
|
||||||
addTearDown(live.dispose);
|
addTearDown(live.dispose);
|
||||||
await t.runAsync(() => live.syncNow(pair));
|
await t.runAsync(() => live.syncNow(pair));
|
||||||
expect(live.toDeleteExcluded[LiveSync.keyOf(pair)], hasLength(3));
|
expect(live.toDeleteExcluded[LiveSync.keyOf(pair)], hasLength(3));
|
||||||
|
expect(live.reasked, contains(LiveSync.keyOf(pair)));
|
||||||
final navKey = GlobalKey<NavigatorState>();
|
final navKey = GlobalKey<NavigatorState>();
|
||||||
final shotKey = GlobalKey();
|
final shotKey = GlobalKey();
|
||||||
await t.pumpWidget(RepaintBoundary(
|
await t.pumpWidget(RepaintBoundary(
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,30 @@ class LiveSync extends ChangeNotifier {
|
||||||
/// 로컬 rsync 쌍은 rsync 가 직접 따르므로 대기 목록 (diff) 에도 같이
|
/// 로컬 rsync 쌍은 rsync 가 직접 따르므로 대기 목록 (diff) 에도 같이
|
||||||
RsyncFilter _filterOf(LiveSyncPair x) => x.method == 'rsync' ? RsyncFilter.parse(c.settings.rsyncOptions) : RsyncFilter.none;
|
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<bool> _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<String> gone(List<String> 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 = <String>{};
|
||||||
|
|
||||||
/// 받는 쪽에만 있는 [path] (상대 경로 [r]) 가 지워지는 모양을 [out] 에 "− 경로" 로 (rsync · _mirrorV 와 같게):
|
/// 받는 쪽에만 있는 [path] (상대 경로 [r]) 가 지워지는 모양을 [out] 에 "− 경로" 로 (rsync · _mirrorV 와 같게):
|
||||||
/// 거른 것은 지우지 않고 (--delete-excluded 면 지움), 안에 거른 것이 든 폴더는 남기고 그 안의 거르지 않은 것만.
|
/// 거른 것은 지우지 않고 (--delete-excluded 면 지움), 안에 거른 것이 든 폴더는 남기고 그 안의 거르지 않은 것만.
|
||||||
/// 10/20 (15, 28 의 장면으로 찾음): 예전에는 그런 폴더도 "− 폴더" 한 줄이라 지울지 묻는 창에 폴더가 통째로 보이고,
|
/// 10/20 (15, 28 의 장면으로 찾음): 예전에는 그런 폴더도 "− 폴더" 한 줄이라 지울지 묻는 창에 폴더가 통째로 보이고,
|
||||||
|
|
@ -375,9 +399,10 @@ class LiveSync extends ChangeNotifier {
|
||||||
|
|
||||||
/// 지울 목록을 보고 결정: [delete] true = 지우기 포함으로 맞추기 (확인함), false = 지우기 끄기. 그 뒤 바로 맞춘다
|
/// 지울 목록을 보고 결정: [delete] true = 지우기 포함으로 맞추기 (확인함), false = 지우기 끄기. 그 뒤 바로 맞춘다
|
||||||
Future<void> decideDelete(LiveSyncPair x, {required bool delete}) async {
|
Future<void> 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));
|
toDelete.remove(keyOf(x));
|
||||||
toDeleteExcluded.remove(keyOf(x));
|
toDeleteExcluded.remove(keyOf(x));
|
||||||
|
reasked.remove(keyOf(x));
|
||||||
await _setPair(x, now);
|
await _setPair(x, now);
|
||||||
await syncNow(now);
|
await syncNow(now);
|
||||||
}
|
}
|
||||||
|
|
@ -478,13 +503,27 @@ class LiveSync extends ChangeNotifier {
|
||||||
final asked = x;
|
final asked = x;
|
||||||
try {
|
try {
|
||||||
// 42: 지우기를 아직 확인하지 않았으면, 지울 것이 있을 때 지우지 않고 맞추고 확인을 기다린다 (목록은 [toDelete])
|
// 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 = <String>[];
|
final excl = <String>[];
|
||||||
final del = [
|
final del = [
|
||||||
for (final d in await diff(x, limit: 100000, filter: _filterOf(x), excluded: excl)) if (d.startsWith('− ')) d.substring(2)
|
for (final d in await diff(x, limit: 100000, filter: _filterOf(x), excluded: excl)) if (d.startsWith('− ')) d.substring(2)
|
||||||
];
|
];
|
||||||
if (del.isEmpty) {
|
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 {
|
} else {
|
||||||
toDelete[k] = del;
|
toDelete[k] = del;
|
||||||
toDeleteExcluded[k] = excl;
|
toDeleteExcluded[k] = excl;
|
||||||
|
|
@ -493,6 +532,7 @@ class LiveSync extends ChangeNotifier {
|
||||||
} else {
|
} else {
|
||||||
toDelete.remove(k);
|
toDelete.remove(k);
|
||||||
toDeleteExcluded.remove(k);
|
toDeleteExcluded.remove(k);
|
||||||
|
reasked.remove(k);
|
||||||
}
|
}
|
||||||
// 지우기 포함이면 원본을 확실히 읽을 수 있을 때만 (rsync --delete · robocopy /PURGE 도). 못 읽으면 멈추고 알린다
|
// 지우기 포함이면 원본을 확실히 읽을 수 있을 때만 (rsync --delete · robocopy /PURGE 도). 못 읽으면 멈추고 알린다
|
||||||
final allowEmpty = _allowEmpty.remove(k);
|
final allowEmpty = _allowEmpty.remove(k);
|
||||||
|
|
|
||||||
|
|
@ -79,21 +79,28 @@ class LiveSyncPair {
|
||||||
/// "지우기" 를 사용자가 지울 목록을 보고 확인했는지. 확인 전에는 지울 것이 있으면 지우지 않고 맞추며 확인을 기다린다 (42).
|
/// "지우기" 를 사용자가 지울 목록을 보고 확인했는지. 확인 전에는 지울 것이 있으면 지우지 않고 맞추며 확인을 기다린다 (42).
|
||||||
/// 이 값이 생기기 전부터 돌던 쌍은 확인한 것으로 본다.
|
/// 이 값이 생기기 전부터 돌던 쌍은 확인한 것으로 본다.
|
||||||
final bool deleteConfirmed;
|
final bool deleteConfirmed;
|
||||||
|
|
||||||
|
/// 지우기를 확인할 때의 rsync 옵션 (rsync 쌍만, 그 밖은 ''). 확인한 뒤 옵션이 바뀌어 지울 것이 늘면 다시 묻는다
|
||||||
|
/// (관리자 3a 판단 10/20 - --delete-excluded · --exclude · --filter 등). null = 이 값이 생기기 전에 확인한 쌍
|
||||||
|
final String? confirmedOptions;
|
||||||
const LiveSyncPair(this.source, this.target,
|
const LiveSyncPair(this.source, this.target,
|
||||||
{this.method = 'builtin',
|
{this.method = 'builtin',
|
||||||
this.delete = false,
|
this.delete = false,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.schedule = const [],
|
this.schedule = const [],
|
||||||
this.deleteConfirmed = false});
|
this.deleteConfirmed = false,
|
||||||
|
this.confirmedOptions});
|
||||||
|
|
||||||
/// [delete] 를 새로 켜면 확인은 다시 받는다
|
/// [delete] 를 새로 켜면 확인은 다시 받는다
|
||||||
LiveSyncPair copyWith({String? method, bool? delete, bool? enabled, List<String>? schedule, bool? deleteConfirmed}) =>
|
LiveSyncPair copyWith(
|
||||||
|
{String? method, bool? delete, bool? enabled, List<String>? schedule, bool? deleteConfirmed, String? confirmedOptions}) =>
|
||||||
LiveSyncPair(source, target,
|
LiveSyncPair(source, target,
|
||||||
method: method ?? this.method,
|
method: method ?? this.method,
|
||||||
delete: delete ?? this.delete,
|
delete: delete ?? this.delete,
|
||||||
enabled: enabled ?? this.enabled,
|
enabled: enabled ?? this.enabled,
|
||||||
schedule: schedule ?? this.schedule,
|
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<String, Object?> toJson() => {
|
Map<String, Object?> toJson() => {
|
||||||
'source': source,
|
'source': source,
|
||||||
|
|
@ -103,6 +110,7 @@ class LiveSyncPair {
|
||||||
'enabled': enabled,
|
'enabled': enabled,
|
||||||
'schedule': schedule,
|
'schedule': schedule,
|
||||||
'deleteConfirmed': deleteConfirmed,
|
'deleteConfirmed': deleteConfirmed,
|
||||||
|
if (confirmedOptions != null) 'confirmedOptions': confirmedOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
factory LiveSyncPair.fromJson(Map<Object?, Object?> j) => LiveSyncPair(
|
factory LiveSyncPair.fromJson(Map<Object?, Object?> j) => LiveSyncPair(
|
||||||
|
|
@ -114,6 +122,7 @@ class LiveSyncPair {
|
||||||
schedule: [for (final x in (j['schedule'] as List?) ?? const []) '$x'],
|
schedule: [for (final x in (j['schedule'] as List?) ?? const []) '$x'],
|
||||||
// 이 값이 없는 예전 쌍은 이미 지우기 포함으로 돌던 것 → 확인한 것으로
|
// 이 값이 없는 예전 쌍은 이미 지우기 포함으로 돌던 것 → 확인한 것으로
|
||||||
deleteConfirmed: j['deleteConfirmed'] as bool? ?? true,
|
deleteConfirmed: j['deleteConfirmed'] as bool? ?? true,
|
||||||
|
confirmedOptions: j['confirmedOptions'] is String ? j['confirmedOptions'] as String : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,8 +90,15 @@ Future<void> confirmLiveDeletes(BuildContext context, LiveSync live, LiveSyncPai
|
||||||
title: Text(trf('대상에서 {0}개를 지울까요?', [list.length])),
|
title: Text(trf('대상에서 {0}개를 지울까요?', [list.length])),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 560,
|
width: 560,
|
||||||
height: 360,
|
// 세로 폰에서도 목록 (특히 거른 것 묶음) 이 더 보이게 화면 높이를 따라 (360 ~ 520)
|
||||||
|
height: (MediaQuery.sizeOf(ctx).height * 0.55).clamp(360.0, 520.0),
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
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}" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).',
|
Text(trf('"{0}" 에는 없고 대상 "{1}" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).',
|
||||||
[vDisplay(x.source), vDisplay(x.target)])),
|
[vDisplay(x.source), vDisplay(x.target)])),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
|
||||||
|
|
@ -114,4 +114,83 @@ void main() {
|
||||||
await tester.tap(find.text('나중에'));
|
await tester.tap(find.text('나중에'));
|
||||||
await tester.pump();
|
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: '틀린 값은 없는 것으로');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,8 @@ void main() {
|
||||||
lf('live/sub/b.txt', 'bb');
|
lf('live/sub/b.txt', 'bb');
|
||||||
rf('mirror/old.txt', 'o');
|
rf('mirror/old.txt', 'o');
|
||||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
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);
|
final pair = LiveSyncPair(p.join(local.path, 'live'), 'dav://$id/mirror', method: 'rsync', delete: true, deleteConfirmed: true);
|
||||||
c.settings.liveSyncPairs = [pair];
|
c.settings.liveSyncPairs = [pair];
|
||||||
final live = LiveSync(c);
|
final live = LiveSync(c);
|
||||||
|
|
@ -359,4 +361,31 @@ void main() {
|
||||||
}
|
}
|
||||||
expect(bad, isEmpty, reason: bad.join('\n'));
|
expect(bad, isEmpty, reason: bad.join('\n'));
|
||||||
}, timeout: const Timeout(Duration(minutes: 5)));
|
}, 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');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue