import '../vfs.dart'; import 'path_rules.dart'; import 'transfer_guard.dart' show FileOpCancelled; // 약속 (나누기 file_ops): 전송 한 번의 설정과 상태 - 취소 · 속도 제한 · 알림 · 같은 이름 처리 · 건너뛴 것. // 로컬 전송 (local_transfer) 과 어느 저장소든 전송 (any_transfer) 이 함께 쓴다. [FileOps] 가 이것을 잇는다. // 입력: 생성자의 설정. 출력: [targetFor] 의 받는 경로 (null = 건너뜀) · 쌓인 목록 ([skipped] · [dirConflicts] · [keptNewer]). // 오류: [check] 는 취소됐으면 [FileOpCancelled]. 저장소 오류는 그대로. /// 복사 · 이동 중 알림 (지금 파일, 지금까지 바이트, 전체 바이트) typedef CopyProgress = void Function(String current, int done, int total); class TransferControl { TransferControl({ this.bandwidthKBps = 0, this.onFileDone, this.onBytes, this.conflict = NameConflict.rename, this.onSettled, this.skipNewerInMerge = false, }); bool _cancel = false; /// 다음 조각에서 멈춘다 void cancel() => _cancel = true; /// 멈추라고 했는지 bool get cancelled => _cancel; /// 속도 제한 (KB/s, 0 = 제한 없음) final int bandwidthKBps; /// 파일 하나를 다 복사할 때마다 (원본 경로) final void Function(String source)? onFileDone; /// 조각을 보낼 때마다 (바이트) - 전송 속도 계산용 final void Function(int bytes)? onBytes; /// 48: 같은 이름이 있을 때 (맨 위 항목 기준 - 덮어쓰기면 폴더 안의 같은 이름 파일도 덮어씀) final NameConflict conflict; /// 건너뛴 항목 (원본 경로) final skipped = []; /// M1 (관리자 P0, 10/19): 원격 맞추기에서 원본의 파일과 같은 이름의 **폴더** 가 받는 쪽에 있어 건너뛴 것. /// --delete 가 없으면 그 폴더를 지우지 않는다 (rsync 도 --delete · --force 없이는 비어 있지 않은 폴더를 지우지 않음) final dirConflicts = []; /// 229: 폴더를 [덮어쓰기] (합치기) 할 때 안쪽에서 받는 쪽이 더 새 파일은 건너뛴다 (환경 설정 "받는 쪽이 더 새것이면 건너뛰기"). /// 맨 위 파일 하나를 사용자가 [덮어쓰기] 로 고른 것은 따른다 (창에 "(있는 것이 더 새것)" 이 보이므로) final bool skipNewerInMerge; /// 229: 위 규칙으로 건너뛴 원본 파일 (이동이어도 원본에 남는다) final keptNewer = []; /// 214: mirror 가 원본 파일 하나를 대상에 맞췄을 때 (지금 복사했거나, 엄격히 같다고 확인함) - 그 원본의 크기 · 바뀐 때. /// 이동은 이것으로 알려 온 파일만 원본에서 지운다 (건너뛴 것 · 도중에 생긴 것은 남긴다) final void Function(String source, int size, DateTime modified)? onSettled; /// [conflict] 에 따라 [destDir] 안의 대상 경로 (null = 건너뜀). /// 같은 자리 (같은 폴더로 복사) 이거나 파일 ↔ 폴더처럼 종류가 다르면 덮어쓰지 않고 새 이름으로. Future targetFor(String src, String destDir) async { final name = vBasename(src); final plain = vJoin(destDir, name); if (conflict == NameConflict.rename || !await vExists(plain)) return vUniqueTarget(destDir, name); if (samePath(src, plain)) return vUniqueTarget(destDir, name); if (conflict == NameConflict.skip) { skipped.add(src); return null; } if (await vIsDir(src) != await vIsDir(plain)) return vUniqueTarget(destDir, name); return plain; } final _clock = Stopwatch(); int _sent = 0; /// 속도 제한: 보낸 양이 허용량보다 앞서면 그만큼 쉰다 Future throttle(int bytes) async { if (bandwidthKBps <= 0) return; if (!_clock.isRunning) _clock.start(); _sent += bytes; final wantMs = _sent * 1000 ~/ (bandwidthKBps * 1024); final ahead = wantMs - _clock.elapsedMilliseconds; if (ahead > 5) await Future.delayed(Duration(milliseconds: ahead)); } /// 취소됐으면 [FileOpCancelled] void check() { if (_cancel) throw const FileOpCancelled(); } }