JJ_MKVMaker/lib/app/sync/sync_commands.dart

96 lines
5.0 KiB
Dart

/// 약속: 실시간 동기화가 부르는 도구 - 앱에 들어 있는 rsync 찾기 · 지금 설정으로 쓸 rsync · 쓸 수 있는 복사 방법 ·
/// 쌍마다 rsync · robocopy 인수 ([SyncCommands]). 지우기 (--delete · /PURGE) 는 쌍의 "지우기 포함" 으로만 넣는다 (97),
/// 원본을 지우는 옵션은 넣지 않는다 (107).
library;
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as p;
import '../../core/file_ops.dart';
import '../../core/sync_preview.dart' show isRsyncDeleteOption, isSourceRemovingOption;
import '../../core/sync_tools.dart';
import '../../platform/windows/rsync_installer.dart';
import '../settings.dart';
/// 앱에 들어 있는 rsync (없으면 null). 시작할 때 [findBundledRsync] 로 정한다.
/// - Windows: 실행 파일 옆 rsync/rsync.exe (tool/fetch_rsync.ps1 로 준비해 빌드에 넣음)
/// - Android: 네이티브 라이브러리 폴더의 librsync.so (tool/build_rsync_android.sh, APK 의 jniLibs)
String? bundledRsync;
Future<void> findBundledRsync() async {
String? path;
if (Platform.isWindows) {
path = p.join(p.dirname(Platform.resolvedExecutable), 'rsync', 'rsync.exe');
} else if (Platform.isAndroid) {
try {
final dir = await const MethodChannel('jj_mkvmaker/android').invokeMethod<String>('nativeLibDir');
if (dir != null) path = p.join(dir, 'librsync.so');
} catch (_) {}
}
bundledRsync = path != null && File(path).existsSync() ? path : null;
}
/// 지금 설정으로 쓸 rsync 실행 파일 (없으면 null).
/// - 직접 지정: 그 파일
/// - 기본: 앱에 들어 있는 것, 없으면 (Windows) 내려받아 둔 것
Future<String?> rsyncExecutable(AppSettings s) async {
if (s.copy.rsyncSource == 'custom') {
return s.copy.rsyncPath.isNotEmpty && File(s.copy.rsyncPath).existsSync() ? s.copy.rsyncPath : null;
}
final b = bundledRsync;
if (b != null && File(b).existsSync()) return b;
if (!Platform.isWindows) return null;
return RsyncInstaller.installedPath();
}
/// 이 기기에서 쓸 수 있는 방법
bool copyMethodAvailable(CopyMethod m, AppSettings s) => switch (m) {
CopyMethod.builtin => true,
CopyMethod.robocopy => Platform.isWindows,
CopyMethod.rsync => Platform.isWindows || s.copy.rsyncSource == 'custom' || bundledRsync != null,
};
class SyncCommands {
/// rsync 인수 (실시간 동기화). 지우기는 쌍의 "지우기 포함" 으로만 - 설정의 rsync 옵션에 --delete · --del 이 있어도
/// 41 · 42 의 확인을 건너뛰지 않게 뺀다 (97)
static List<String> rsyncArgs(LiveSyncPair x, AppSettings s, {required bool windows}) => [
// 107: 원본을 지우는 옵션 (--remove-source-files) 도 실시간 동기화에서는 쓰지 않는다
// 관리자 판단 (10/20, 28 확인): 목록 · 기록 · 배치 파일의 C:\… 는 rsync 가 읽는 꼴로 (rsyncFileOptionArgs)
...rsyncFileOptionArgs([
for (final o in splitOptions(s.copy.rsyncOptions))
if (o != '-P' && o != '--progress' && !isRsyncDeleteOption(o) && !isSourceRemovingOption(o)) o,
], windows: windows),
if (!windows) '-8', // 한글 등 이름을 \#355… 로 바꾸지 않고 그대로 (Android 빌드는 iconv 없음)
if (s.copy.bandwidthKBps > 0) '--bwlimit=${s.copy.bandwidthKBps}',
if (x.delete) '--delete',
// 10/20 (15, 28 의 장면으로 찾음): --delete-excluded 는 쌍이 지우기 포함일 때만 남긴다 - 예전에는 늘 빼서, 지울지 묻는 창
// (diff 는 옵션의 거르기를 따름) 에 보인 거른 것을 실제로는 지우지 않고 맞출 것에 계속 남았다 (원격 쌍 · Rsync 화면은 따름)
if (x.delete && splitOptions(s.copy.rsyncOptions).contains('--delete-excluded')) '--delete-excluded',
...appTempRsyncExcludes, // 78 보충: 이름 바꾸는 중의 임시 이름은 보내지도 지우지도 않는다
'${toCygwinPath(x.source, windows: windows)}/',
'${rsyncDestPath(x.target, windows: windows)}/', // 받는 쪽 UNC 는 \\서버\… (28 · 15 확인 10/20)
];
/// robocopy 인수 (실시간 동기화). 지우기 (/MIR · /PURGE) 는 쌍의 "지우기 포함" 으로만 (/MIR 는 /E 로) - 97
static List<String> robocopyArgs(LiveSyncPair x, AppSettings s) {
final opts = [
for (final o in splitOptions(s.copy.robocopyOptions))
if (o.toUpperCase() == '/MIR') '/E' else if (o.toUpperCase() != '/PURGE' && !isSourceRemovingOption(o)) o,
];
return [
x.source,
x.target,
...opts,
if (!opts.any((o) => o.toUpperCase() == '/E')) '/E',
// 데이터: 링크 · junction 은 따라가지 않는다 (복사 · 이동과 같게 - robocopyExtraOptions)
if (!robocopyLinkOption(opts)) '/XJ',
if (x.delete) '/PURGE',
...appTempRobocopyExcludes, // 78 보충
if (s.copy.bandwidthKBps > 0) '/IPG:${robocopyIpg(s.copy.bandwidthKBps)}',
'/BYTES', '/NJH', '/NJS', '/NDL', '/NP',
];
}
}