// 약속 (나누기 sync_tools): rsync · robocopy 출력 읽기 - 조각을 받을 때마다 "다 보낸 파일" 줄과 진행률 · 속도 · 확인한 수. // 입력: 출력 조각 (줄 중간에서 끊겨도 됨 - 남은 것은 다음 조각과 이어 읽음). 출력: 다 보낸 파일 (원본 기준 상대 경로) 목록. // 오류: 던지지 않는다 (모르는 줄은 건너뜀). /// rsync -v 출력에서 "다 보낸 파일" 줄을 골라낸다 (-P 의 \r 진행 줄 · 머리글 · 요약 · 폴더 줄은 뺌). /// 결과: 원본 기준 상대 경로 (예: "sub/inner.txt") class RsyncOutput { final _buf = StringBuffer(); /// 지금 파일의 진행률 (-P 의 " 1,234 45% 1.2MB/s" 줄) double? currentPercent; /// 지금 전송 속도 (바이트/초, 같은 줄의 "1.2MB/s") double? currentSpeed; /// 폴더 전체에서 확인한 항목 (진행 줄의 "to-chk=남은/전체" · "ir-chk=…"). 모르면 null. int? checked, checkTotal; static final _chkRx = RegExp(r'(?:to|ir)-chk=(\d+)/(\d+)'); static final _speedRx = RegExp(r'(\d+(?:\.\d+)?)([kKMGT]?)B/s'); List feed(String chunk) { _buf.write(chunk); final text = _buf.toString(); final cut = text.lastIndexOf(RegExp(r'[\r\n]')); if (cut < 0) return const []; _buf ..clear() ..write(text.substring(cut + 1)); final done = []; for (final raw in text.substring(0, cut).split(RegExp(r'[\r\n]'))) { final line = raw.trimRight(); if (line.isEmpty) continue; if (line.startsWith(' ')) { final m = RegExp(r'\s(\d{1,3})%\s').firstMatch(line); if (m != null) currentPercent = int.parse(m[1]!) / 100; final ck = _chkRx.firstMatch(line); if (ck != null) { checkTotal = int.parse(ck[2]!); checked = checkTotal! - int.parse(ck[1]!); } final sp = _speedRx.firstMatch(line); if (sp != null) { const mul = {'': 1, 'k': 1024, 'K': 1024, 'M': 1024 * 1024, 'G': 1024 * 1024 * 1024, 'T': 1024 * 1024 * 1024 * 1024}; currentSpeed = double.parse(sp[1]!) * mul[sp[2]!]!; } continue; } if (line.endsWith('/')) continue; // 폴더 if (line.startsWith('sending ') || line.startsWith('receiving ') || line.startsWith('sent ') || line.startsWith('total size') || line.startsWith('created directory') || line.startsWith('deleting ') || line.startsWith('rsync:') || line.startsWith('rsync error') || line.contains(' -> ')) { continue; } currentPercent = null; done.add(line); } return done; } } /// robocopy 출력에서 파일 줄 ("New File", "Newer", "Older", "Changed" … 뒤의 크기 · 이름) 을 센다 class RobocopyOutput { final _buf = StringBuffer(); double? currentPercent; /// 파일 줄: 탭 · 종류 · 탭 탭 · 크기 · 탭 · 경로. 61: 종류 글은 Windows 언어마다 다르므로 /// ("New File" · "새 파일" · "新しいファイル" · "新文件") 글이 아니라 탭 모양으로 알아본다. /// 종류가 * 로 시작하면 (*EXTRA File 등) 대상에만 있는 것이라 세지 않는다. static final _file = RegExp(r'^\t+\s*([^\t\s\d][^\t]*?)\s*\t+\s*[\d.]+\s*[kmgt]?\s*\t(.+)$', caseSensitive: false); List feed(String chunk) { _buf.write(chunk); final text = _buf.toString(); final cut = text.lastIndexOf(RegExp(r'[\r\n]')); if (cut < 0) return const []; _buf ..clear() ..write(text.substring(cut + 1)); final done = []; for (final raw in text.substring(0, cut).split(RegExp(r'[\r\n]'))) { final line = raw.trim(); final pct = RegExp(r'^(\d{1,3}(?:\.\d)?)%$').firstMatch(line); if (pct != null) { currentPercent = double.parse(pct[1]!) / 100; continue; } final m = _file.firstMatch(raw); if (m != null && !m[1]!.startsWith('*')) { currentPercent = null; done.add(m[2]!.trim()); } } return done; } }