94 lines
4.1 KiB
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();
|
|
}
|
|
}
|