import '../core/file_ops.dart' show isSameOrInside, samePath, transferProblem; import '../core/rsync_filter.dart'; import '../core/sync_preview.dart'; import '../core/sync_tools.dart' show OptionProblem, optionProblems, withUpdateOption; import '../l10n/tr.dart'; import 'copy_center.dart'; import 'settings.dart' show CopyTask; import 'transfer_job.dart'; /// 약속 (E5c): Rsync 화면의 [→] · [←] · [⇄] 실행. 화면 (확인 창) 은 이 객체만 부른다. /// rsync 옵션 · 미리 보기 · 실행은 sync_preview · sync_tools · rsync_filter · CopyCenter 를 부르기만 한다 (rsync 를 흉내 내지 않음). /// - [RsyncRun.prepare]: 왼쪽 · 오른쪽 고른 폴더 · 방향 → 실행할 쌍 또는 막는 이유 (글). 원본 · 대상이 없어졌으면 [missing]. /// 막는 것: 고르지 않음 · 대상이 원본과 같거나 원본 안 · (설정이 막으면) 원본이 대상 안 · 같은 쌍이 도는 중 · /// 원본 · 대상 없음 · 원본이 대상 안인데 지우기 (34 · 23 · 105). /// - 확인 창에서 [removeSource] (원본 파일 지우기) · [prune] 을 바꾸면 [tasks] 가 그 작업으로 바뀐다 (96: 창 · 미리 보기 · 실행이 같은 옵션). /// - ⇄ 는 → 다음에 ← (95), ← 에서는 지우지 않는다 · 받는 쪽이 새것이면 덮지 않음 (-u). /// - [execute]: 확인 창에 보인 옵션 그대로 차례로 실행 (앞의 것이 실패 · 취소되면 뒤는 하지 않음). 출력 = 시작한 작업들 · /// 시작하지 못한 이유. /// - 15 의 RsyncPlan · RsyncLaunch (modular_sync) 가 생기면 그것을 부르게 맞춘다. class RsyncRun { RsyncRun._(this.center, this.l, this.r, this.runs, this.both, this.nested, this.optionMove, this.viaDav, this.copySkipNewer) : removeSource = optionMove && !both { tasks = _shown(removeSource); } final CopyCenter center; final String l, r; final List<(String, String)> runs; final bool both, nested, viaDav, copySkipNewer; /// 환경 설정 (또는 기억한) 옵션에 원본 지우기가 있는지 (178) final bool optionMove; /// 한 방향만: 원본 파일 지우기 (--remove-source-files) · 끝난 뒤 원본 정리 bool removeSource; String prune = 'keep'; /// 확인 창에 보이고 실행할 작업 (쌍마다) late List tasks; List _shown(bool move) => [for (final (src, dst) in runs) center.peek([src], dst, contents: true, method: 'rsync', move: move)]; static ({RsyncRun? run, String? problem, bool missing}) prepare( CopyCenter center, { required String? l, required String? r, required bool right, required bool left, required bool allowInnerToOuter, required Iterable jobs, required bool viaDav, required bool copySkipNewer, }) { ({RsyncRun? run, String? problem, bool missing}) no(String why, {bool missing = false}) => (run: null, problem: why, missing: missing); if (l == null || r == null) return no(tr('왼쪽 · 오른쪽 창에서 폴더를 하나씩 고르세요.')); final runs = [if (right) (l, r), if (left) (r, l)]; if (runs.any((x) => isSameOrInside(x.$2, x.$1))) { return no(tr('대상이 원본과 같거나 원본 안에 있습니다 (맞출 때마다 원본이 바뀝니다). 다른 폴더를 고르세요.')); } final nested = runs.any((x) => isSameOrInside(x.$1, x.$2)); if (nested && !allowInnerToOuter) return no(tr('원본이 대상 안에 있습니다 (환경 설정에서 막아 둠). 다른 폴더를 고르세요.')); if (jobs.any((j) => !j.finished && runs.any((x) => (samePath(j.sources.first, x.$1) && samePath(j.dest, x.$2)) || (samePath(j.sources.first, x.$2) && samePath(j.dest, x.$1))))) { return no(tr('이 두 폴더는 지금 rsync 하는 중입니다. 끝난 뒤에 하세요.')); } for (final (src, dst) in runs) { final problem = transferProblem([src], dst, move: false); if (problem != null) return no(problem, missing: true); } final both = right && left; final optionMove = runs .map((x) => center.peek([x.$1], x.$2, contents: true, method: 'rsync', move: false)) .any((t) => optionsRemoveSource(t.options)); final run = RsyncRun._(center, l, r, runs, both, nested, optionMove, viaDav, copySkipNewer); if (run.nestedDelete()) return no(nestedDeleteMessage); return (run: run, problem: null, missing: false); } static String get nestedDeleteMessage => tr('원본이 대상 안에 있으면 지우기 (--delete) 를 함께 쓸 수 없습니다 (대상의 다른 파일이 모두 지워집니다).'); /// 원본 파일 지우기를 바꿈 - 작업 (옵션) 이 그 작업으로 바뀐다 void setRemoveSource(bool v) { removeSource = v; tasks = _shown(v); } String opts(CopyTask t) { final o = withoutSourceRemovingOptions(t.options); return both ? withUpdateOption(o) : o; } /// 95: ⇄ 는 → 다음에 ← (차례로). ← 에서는 지우지 않는다 (→ 가 방금 복사한 것을 지우지 않게) String runOpts(int i) => both && i > 0 ? withoutDeleteOptions(opts(tasks[i])) : opts(tasks[i]); /// 15 검토 F2 · 관리자 지시 (10/19): 미리 볼 수 없는 거르기 규칙 List filterLimits() => {for (final t in tasks) ...RsyncFilter.parse(opts(t)).unsupported}.toList(); /// 관리자 지시 (10/20): 고쳐 읽어도 알아볼 수 없는 옵션 (있으면 [실행] 을 막음) List badOptions() { final seen = {}; return [for (final t in tasks) ...optionProblems(opts(t)).where((x) => seen.add(x.text))]; } /// 93: 지우는 실행 (--delete 등) bool deletes() => tasks.any((t) => optionsDelete(opts(t))); /// 105: 원본이 대상 안인데 지우기가 들어가면 막는다 bool nestedDelete() => nested && deletes(); /// 72 · 177 · 229: 실행 전 비교 (rsync 로 맞추면 옵션의 거르기도, WebDAV 등은 앱이 직접) Future> preview() async => both ? previewBoth(l, r, delete: deletes(), filter: RsyncFilter.parse(opts(tasks.first))) : [ for (final t in tasks) ...await previewSync(t.sources.first, t.dest, toRight: t.sources.first == l, delete: deletes(), update: optionsUpdate(opts(t)) || (viaDav && copySkipNewer), skipLinks: viaDav, filter: RsyncFilter.parse(opts(t))), ]; /// 확인 창에 보인 옵션으로 차례로 실행. [onStarted] 는 작업이 시작될 때마다, [alive] 가 false 면 멈춘다 Future<({List jobs, String? notStarted})> execute({ required String? rsyncExe, required void Function(TransferJob job) onStarted, required bool Function() alive, void Function(String line)? note, }) async { final shownOpts = [for (var i = 0; i < tasks.length; i++) runOpts(i)]; var run = [ for (final (src, dst) in runs) await center.remember([src], dst, contents: true, method: 'rsync', move: removeSource), ]; if (removeSource) { run = [for (final t in run) t.copyWith(prune: prune)]; for (final t in run) { await center.update(t); } } final jobs = []; for (var i = 0; i < run.length; i++) { note?.call(trf('rsync 시작 ({0}): {1} → {2}', [tr('Rsync 화면 확인 창의 [실행]'), run[i].sources.first, run[i].dest])); // 218: 확인 창에 보인 그대로 rsync 로 (WebDAV 가 끼면 앱이 직접) final j = await center.start(run[i].copyWith(options: shownOpts[i], method: 'rsync'), rsyncExe: viaDav ? null : rsyncExe); if (!alive()) break; if (j == null) { final why = transferProblem(run[i].sources, run[i].dest, move: run[i].move, contents: run[i].contents); note?.call(trf('rsync 시작하지 못함: {0}', [why ?? ''])); return (jobs: jobs, notStarted: why ?? tr('rsync 를 실행할 수 없습니다')); } jobs.add(j); onStarted(j); await j.done; if (j.error != null) break; } return (jobs: jobs, notStarted: null); } }