JJ_MKVMaker/lib/app/rsync_runner.dart

165 lines
8.0 KiB
Dart

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<CopyTask> tasks;
List<CopyTask> _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<TransferJob> 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<String> filterLimits() => {for (final t in tasks) ...RsyncFilter.parse(opts(t)).unsupported}.toList();
/// 관리자 지시 (10/20): 고쳐 읽어도 알아볼 수 없는 옵션 (있으면 [실행] 을 막음)
List<OptionProblem> badOptions() {
final seen = <String>{};
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<List<PreviewItem>> 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<TransferJob> 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 = <TransferJob>[];
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);
}
}