import 'dart:convert' show utf8; import 'dart:io'; import 'package:jj_mkvmaker/core/sync_tools.dart' show splitOptions, toCygwinPath; import 'package:path/path.dart' as p; /// 28 (10/19) 이 원격 맞추기 vs 실제 rsync 비교에 쓴 장면 · 옵션 (rsync_filter_test 의 "28 의 장면" 과 같은 것). /// 원격 (WebDAV · SFTP · FTP · SMB) 맞추기의 결과 트리를 실제 rsync 를 로컬에서 돌린 결과 트리와 비교할 때 쓴다. /// 보내는 쪽 파일 const sceneSrc = [ 'a.txt', 'b.log', 'build/x.o', 'build/sub/y.o', 'sub/c.txt', 'sub/deep/d.txt', 'sub/deep/e.log', 'node_modules/m/i.js', 'src/node_modules/n.js', 'docs/readme.md', 'docs/img/p.png', '한글 폴더/가.txt', 'space dir/s.bak', 'Keep.TXT', 'tmp/t.txt', 'x/tmp/t2.txt', 'foo/bar', 'foo/q/bar', ]; /// 받는 쪽에만 있는 파일 (지우기 · 거른 것 지키기 · 거른 것이 든 폴더) const sceneDst = [ 'extra.txt', 'keep.log', 'build/old.o', 'old/keep.log', 'old/gone.txt', 'sub/deep/z.txt', 'tmp/old.txt', 'onlydst/a.bak', 'old2/deep/x/keep.log', 'old2/deep/x/g.txt', 'old2/deep/y/h.txt', 'old2/e.txt', ]; /// 옵션 (그대로 rsync 에 · 원격 맞추기는 그 옵션의 지우기 · 거르기) const sceneOptions = [ '-a --delete --exclude=*.log --exclude=build/ --exclude=/sub/deep/', '-a --delete --exclude=*.log', '-a --delete --include=*/ --include=*.txt --exclude=*', '-a --delete --exclude=node_modules/', '-a --delete --exclude=/tmp/', '-a --delete --exclude=tmp/', '-a --delete --exclude=sub/deep', '-a --delete --exclude=docs/***', '-a --delete --exclude=foo/**/bar', '-a --delete --exclude=**/bar', '-a --delete --exclude=*.[lb][oa][gk]', '-a --delete --exclude="space dir/"', '-a --delete --exclude=한글*/', '-a --delete --exclude=*.bak --delete-excluded', '-a --delete --exclude=*.log --delete-excluded', '-a --delete --filter="- *.o" --filter="+ build/"', '-a --delete --include=sub/deep/d.txt --exclude=sub/deep/*', '-a --delete --exclude=?.txt', '-a --delete --exclude=*.TXT', '-a --delete --exclude=deep/', '-a --delete --exclude=/old2/deep/x/keep.log', '-a --del', '-a --delete-after --exclude=*.log', '-a --delete --exclude=**/x/', '-a --delete --include=*.txt --exclude=*.log --exclude=*/', '-a --delete -f"+ *.txt" --exclude=*', '-a --delete -f"- *.log"', // 지우기 없음 (거른 것만) '-a --exclude=*.log --exclude=build/', ]; /// 앱에 들어 있는 rsync (Windows). 없으면 null String? get bundledRsync { final f = p.join(Directory.current.path, 'third_party', 'rsync', 'windows', 'rsync.exe'); return Platform.isWindows && File(f).existsSync() ? f : null; } /// 장면을 만든다 (있던 것은 지우고). 보내는 쪽 글은 "s:경로", 받는 쪽은 "d:경로" void writeScene(String src, String dst) { for (final (dir, list, tag) in [(src, sceneSrc, 's'), (dst, sceneDst, 'd')]) { final d = Directory(dir); if (d.existsSync()) d.deleteSync(recursive: true); d.createSync(recursive: true); for (final r in list) { File(p.join(dir, r)) ..createSync(recursive: true) ..writeAsStringSync('$tag:$r'); } } } /// 폴더 안의 트리: 상대 경로 (/) → 폴더면 "/", 파일이면 그 글 Map treeOf(String dir) { final out = {}; final root = Directory(dir); if (!root.existsSync()) return out; for (final e in root.listSync(recursive: true)) { final rel = p.relative(e.path, from: dir).replaceAll(r'\', '/'); out[rel] = e is Directory ? '/' : File(e.path).readAsStringSync(); } return out; } /// 실제 rsync 로 src/ → dst/ 를 맞춘 뒤 dst 의 트리 (src · dst 는 writeScene 으로 만든 로컬 폴더) Future> realRsyncTree(String rsync, String src, String dst, String opts) async { final r = await Process.run(rsync, ['-8', ...splitOptions(opts), '${toCygwinPath(src)}/', '${toCygwinPath(dst)}/'], stdoutEncoding: utf8, stderrEncoding: utf8); if (r.exitCode != 0) throw StateError('rsync $opts → ${r.exitCode}: ${r.stderr}'); return treeOf(dst); } /// 두 트리의 다른 점 (없으면 빈 목록) List treeDiff(Map want, Map got) => [ for (final k in {...want.keys, ...got.keys}.toList()..sort()) if (want[k] != got[k]) '$k: rsync=${want[k] == null ? '없음' : want[k] == '/' ? '폴더' : '"${want[k]}"'} · 앱=${got[k] == null ? '없음' : got[k] == '/' ? '폴더' : '"${got[k]}"'}', ];