/// 약속: 실시간 동기화가 부르는 도구 - 앱에 들어 있는 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 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('nativeLibDir'); if (dir != null) path = p.join(dir, 'librsync.so'); } catch (_) {} } bundledRsync = path != null && File(path).existsSync() ? path : null; } /// 지금 설정으로 쓸 rsync 실행 파일 (없으면 null). /// - 직접 지정: 그 파일 /// - 기본: 앱에 들어 있는 것, 없으면 (Windows) 내려받아 둔 것 Future 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 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 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', ]; } }