168 lines
8.3 KiB
Dart
168 lines
8.3 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';
|
|
import '../core/files/same_file.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,
|
|
// F2: 앱이 직접 (원격) 맞출 때는 실제 맞추기와 같은 판단 (이동이면 거의 같은 때만)
|
|
same: viaDav ? SameFile.forAppSync(remote: true, move: optionMove) : const SameFile(),
|
|
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);
|
|
}
|
|
}
|