216 lines
8.1 KiB
Dart
216 lines
8.1 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:path/path.dart' as p;
|
|
|
|
import '../../l10n/tr.dart';
|
|
import '../vfs.dart';
|
|
import 'path_rules.dart';
|
|
import 'transfer_control.dart';
|
|
import 'same_file.dart';
|
|
|
|
// 약속 (나누기 file_ops): 로컬 (같은 기기의 폴더 · 네트워크 공유) 끼리의 복사 · 이동 · 맞추기 · 지우기 · 크기.
|
|
// 입력: 설정 · 상태 [TransferControl], 원본 경로들 · 받는 폴더. 출력: 만든 경로들 · 복사한 파일 수.
|
|
// 오류: 취소 → [FileOpCancelled] (만들다 만 파일은 지움) · 폴더를 자기 안으로 → [FileSystemException] ·
|
|
// 맞추기에서 원본 폴더를 읽지 못함 → [SourceUnreadableException] (그 폴더에서는 아무것도 지우지 않음). 그 밖의 파일 오류는 그대로.
|
|
// WebDAV 등이 섞이면 any_transfer 를 쓴다 (고르는 것은 [FileOps]).
|
|
|
|
/// 전체 크기 (진행률용)
|
|
Future<int> localTotalSize(List<String> paths) async {
|
|
var total = 0;
|
|
for (final s in paths) {
|
|
final t = FileSystemEntity.typeSync(s, followLinks: false);
|
|
if (t == FileSystemEntityType.directory) {
|
|
await for (final e in Directory(s).list(recursive: true, followLinks: false).handleError((_) {})) {
|
|
if (e is File) {
|
|
try {
|
|
total += await e.length();
|
|
} catch (_) {}
|
|
}
|
|
}
|
|
} else if (t == FileSystemEntityType.file) {
|
|
total += await File(s).length();
|
|
}
|
|
}
|
|
return total;
|
|
}
|
|
|
|
/// [sources] 를 [destDir] 로 복사 (같은 이름이 있으면 [TransferControl.conflict] 대로). 만든 경로들을 돌려준다.
|
|
Future<List<String>> localCopy(TransferControl c, List<String> sources, String destDir, {CopyProgress? onProgress}) async {
|
|
final total = await localTotalSize(sources);
|
|
var done = 0;
|
|
final made = <String>[];
|
|
// 43: 임시 이름 (.jjpart) 에 다 쓴 뒤 이름을 바꾼다 - 복사 도중 앱이 끝나도 반쯤 쓴 파일이 완성된 이름으로 남지 않게
|
|
Future<void> copyFile(File f, String finalTarget) async {
|
|
c.check();
|
|
final target = '$finalTarget.jjpart';
|
|
final out = File(target).openWrite();
|
|
try {
|
|
try {
|
|
await for (final chunk in f.openRead()) {
|
|
c.check();
|
|
out.add(chunk);
|
|
done += chunk.length;
|
|
onProgress?.call(f.path, done, total);
|
|
c.onBytes?.call(chunk.length);
|
|
await c.throttle(chunk.length);
|
|
}
|
|
} finally {
|
|
await out.close();
|
|
}
|
|
c.check();
|
|
} catch (_) {
|
|
// 취소 · 실패: 만들다 만 파일을 남기지 않는다
|
|
try {
|
|
await File(target).delete();
|
|
} catch (_) {}
|
|
rethrow;
|
|
}
|
|
// 덮어쓰기: 새 파일을 다 쓴 뒤에야 옛 파일을 지운다 (도중에 실패하면 옛 파일이 남게)
|
|
if (File(finalTarget).existsSync()) await File(finalTarget).delete();
|
|
await File(target).rename(finalTarget);
|
|
try {
|
|
await File(finalTarget).setLastModified(await f.lastModified());
|
|
} catch (_) {}
|
|
c.onFileDone?.call(f.path);
|
|
}
|
|
|
|
Future<void> copyAny(String src, String target, {bool inner = false}) async {
|
|
if (FileSystemEntity.isDirectorySync(src)) {
|
|
await Directory(target).create(recursive: true);
|
|
await for (final e in Directory(src).list(followLinks: false)) {
|
|
await copyAny(e.path, p.join(target, p.basename(e.path)), inner: true);
|
|
}
|
|
} else {
|
|
// 229: 합치는 폴더 안에서 받는 쪽이 더 새것이면 예전 것으로 덮지 않는다
|
|
if (inner && c.skipNewerInMerge && await isNewerThere(src, target)) {
|
|
c.keptNewer.add(src);
|
|
return;
|
|
}
|
|
await copyFile(File(src), target);
|
|
}
|
|
}
|
|
|
|
for (final s in sources) {
|
|
if (FileSystemEntity.isDirectorySync(s) && isSameOrInside(destDir, s)) {
|
|
throw FileSystemException(tr('폴더를 자기 안으로 복사할 수 없습니다'), s);
|
|
}
|
|
final target = await c.targetFor(s, destDir);
|
|
if (target == null) continue;
|
|
await copyAny(s, target);
|
|
made.add(target);
|
|
}
|
|
return made;
|
|
}
|
|
|
|
/// 229: [dir] 안에서 [keep] 의 파일만 남기고 지운다 (비게 된 폴더도). [dir] 자체는 남는 것이 있으면 둔다
|
|
Future<void> _deleteExcept(String dir, Set<String> keep) async {
|
|
Future<bool> walk(Directory d) async {
|
|
var empty = true;
|
|
await for (final e in d.list(followLinks: false)) {
|
|
if (e is Directory) {
|
|
if (await walk(e)) {
|
|
await e.delete();
|
|
} else {
|
|
empty = false;
|
|
}
|
|
} else if (keep.contains(p.normalize(e.path))) {
|
|
empty = false;
|
|
} else {
|
|
await e.delete();
|
|
}
|
|
}
|
|
return empty;
|
|
}
|
|
|
|
if (await walk(Directory(dir))) await Directory(dir).delete();
|
|
}
|
|
|
|
/// 229: [target] 이 있고 [src] 보다 2초 넘게 새것인지
|
|
Future<bool> isNewerThere(String src, String target) async {
|
|
final t = File(target);
|
|
if (!await t.exists()) return false;
|
|
return SameFile.keepNewer(skipNewer: true, targetModified: await t.lastModified(), modified: await File(src).lastModified());
|
|
}
|
|
|
|
/// 229: 같은 이름 창 - 폴더를 합치면 안쪽에서 받는 쪽이 더 새 파일 수 ([limit] 개까지 셈, 로컬만)
|
|
Future<int> countNewerInside(String src, String dst, {int limit = 5000}) async {
|
|
var n = 0, seen = 0;
|
|
Future<void> walk(String s, String d) async {
|
|
await for (final e in Directory(s).list(followLinks: false).handleError((_) {})) {
|
|
if (seen++ > limit) return;
|
|
final t = p.join(d, p.basename(e.path));
|
|
if (e is Directory) {
|
|
if (FileSystemEntity.isDirectorySync(t)) await walk(e.path, t);
|
|
} else if (await isNewerThere(e.path, t)) {
|
|
n++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (FileSystemEntity.isDirectorySync(src) && FileSystemEntity.isDirectorySync(dst)) await walk(src, dst);
|
|
return n;
|
|
}
|
|
|
|
/// 이동: 같은 드라이브면 이름 바꾸기, 아니면 복사한 뒤 원본 삭제
|
|
Future<List<String>> localMove(TransferControl c, List<String> sources, String destDir, {CopyProgress? onProgress}) async {
|
|
final made = <String>[];
|
|
for (final s in sources) {
|
|
c.check();
|
|
if (isSameOrInside(destDir, s)) throw FileSystemException(tr('폴더를 자기 안으로 옮길 수 없습니다'), s);
|
|
if (samePath(p.dirname(s), destDir)) {
|
|
made.add(s); // 이미 그 폴더에 있음
|
|
continue;
|
|
}
|
|
final target = await c.targetFor(s, destDir);
|
|
if (target == null) continue;
|
|
final exists = FileSystemEntity.typeSync(target) != FileSystemEntityType.notFound;
|
|
if (exists && FileSystemEntity.isDirectorySync(s)) {
|
|
// 덮어쓰기로 폴더를 합친다: 복사 (안의 같은 이름은 덮어씀) 뒤 원본 삭제
|
|
final keptBefore = c.keptNewer.length;
|
|
made.addAll(await localCopy(c, [s], destDir, onProgress: onProgress));
|
|
c.check();
|
|
final kept = c.keptNewer.sublist(keptBefore);
|
|
if (kept.isEmpty) {
|
|
await deletePaths(c, [s]);
|
|
} else {
|
|
// 229: 건너뛴 (받는 쪽이 더 새) 원본은 남기고 옮긴 것만 지운다
|
|
await _deleteExcept(s, kept.map(p.normalize).toSet());
|
|
}
|
|
continue;
|
|
}
|
|
try {
|
|
final t = FileSystemEntity.typeSync(s);
|
|
// 덮어쓰기 (파일): 이름 바꾸기가 옛 파일을 바로 바꿔 놓는다 (다른 드라이브면 아래 복사가 덮어씀)
|
|
if (t == FileSystemEntityType.directory) {
|
|
await Directory(s).rename(target);
|
|
} else {
|
|
await File(s).rename(target);
|
|
}
|
|
made.add(target);
|
|
} on FileSystemException {
|
|
// 다른 드라이브 · 저장 장치: 복사 후 삭제
|
|
final copied = await localCopy(c, [s], destDir, onProgress: onProgress);
|
|
await deletePaths(c, [s]);
|
|
made.addAll(copied);
|
|
}
|
|
}
|
|
return made;
|
|
}
|
|
|
|
/// 지우기 (로컬 · WebDAV). 없는 것은 그냥 지나간다. 항목마다 취소를 본다
|
|
Future<void> deletePaths(TransferControl c, List<String> paths) async {
|
|
for (final s in paths) {
|
|
c.check();
|
|
if (isDav(s)) {
|
|
await vDelete(s);
|
|
continue;
|
|
}
|
|
final t = FileSystemEntity.typeSync(s, followLinks: false);
|
|
if (t == FileSystemEntityType.directory) {
|
|
await Directory(s).delete(recursive: true);
|
|
} else if (t != FileSystemEntityType.notFound) {
|
|
await File(s).delete();
|
|
}
|
|
}
|
|
}
|