JJ_MKVMaker/lib/core/files/transfer_control.dart

94 lines
4.1 KiB
Dart

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 = <String>[];
/// M1 (관리자 P0, 10/19): 원격 맞추기에서 원본의 파일과 같은 이름의 **폴더** 가 받는 쪽에 있어 건너뛴 것.
/// --delete 가 없으면 그 폴더를 지우지 않는다 (rsync 도 --delete · --force 없이는 비어 있지 않은 폴더를 지우지 않음)
final dirConflicts = <String>[];
/// 229: 폴더를 [덮어쓰기] (합치기) 할 때 안쪽에서 받는 쪽이 더 새 파일은 건너뛴다 (환경 설정 "받는 쪽이 더 새것이면 건너뛰기").
/// 맨 위 파일 하나를 사용자가 [덮어쓰기] 로 고른 것은 따른다 (창에 "(있는 것이 더 새것)" 이 보이므로)
final bool skipNewerInMerge;
/// 229: 위 규칙으로 건너뛴 원본 파일 (이동이어도 원본에 남는다)
final keptNewer = <String>[];
/// 214: mirror 가 원본 파일 하나를 대상에 맞췄을 때 (지금 복사했거나, 엄격히 같다고 확인함) - 그 원본의 크기 · 바뀐 때.
/// 이동은 이것으로 알려 온 파일만 원본에서 지운다 (건너뛴 것 · 도중에 생긴 것은 남긴다)
final void Function(String source, int size, DateTime modified)? onSettled;
/// [conflict] 에 따라 [destDir] 안의 대상 경로 (null = 건너뜀).
/// 같은 자리 (같은 폴더로 복사) 이거나 파일 ↔ 폴더처럼 종류가 다르면 덮어쓰지 않고 새 이름으로.
Future<String?> 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<void> 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<void>.delayed(Duration(milliseconds: ahead));
}
/// 취소됐으면 [FileOpCancelled]
void check() {
if (_cancel) throw const FileOpCancelled();
}
}