119 lines
6.1 KiB
Dart
119 lines
6.1 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:path/path.dart' as p;
|
|
|
|
import '../../l10n/tr.dart';
|
|
import '../vfs.dart';
|
|
|
|
// 약속 (나누기 file_ops): 경로 규칙 - 같은 경로 · 안에 있음 · 바깥 것만 · 짧은 표시 · 겹치지 않는 이름 · 전송 전 확인 · 앱 임시 이름.
|
|
// 입력: 경로 글 (로컬 · WebDAV). 출력: 참/거짓 · 경로 · 알릴 글 (문제 없으면 null).
|
|
// 오류: 던지지 않는다. [uniqueTarget] · [transferProblem] 만 로컬 파일 시스템을 본다 (없으면 없는 대로).
|
|
|
|
/// 48: 받는 폴더에 같은 이름이 있을 때
|
|
enum NameConflict {
|
|
/// "이름 (2).확장자" 로 (예전 동작)
|
|
rename,
|
|
|
|
/// 덮어쓰기 (폴더는 합치고 안의 같은 이름 파일을 덮어씀)
|
|
overwrite,
|
|
|
|
/// 건너뛰기 (그 항목은 복사 · 이동하지 않음)
|
|
skip,
|
|
}
|
|
|
|
/// 같은 이름이 있으면 "이름 (2).확장자" 로
|
|
String uniqueTarget(String dir, String name) {
|
|
var target = p.join(dir, name);
|
|
if (FileSystemEntity.typeSync(target) == FileSystemEntityType.notFound) return target;
|
|
final base = p.basenameWithoutExtension(name), ext = p.extension(name);
|
|
for (var i = 2;; i++) {
|
|
target = p.join(dir, '$base ($i)$ext');
|
|
if (FileSystemEntity.typeSync(target) == FileSystemEntityType.notFound) return target;
|
|
}
|
|
}
|
|
|
|
/// 같은 경로인지 (Windows 는 대소문자 무시)
|
|
bool samePath(String a, String b) {
|
|
final dav = davSame(a, b);
|
|
if (dav != null) return dav;
|
|
final x = p.normalize(p.absolute(a)), y = p.normalize(p.absolute(b));
|
|
return Platform.isWindows ? x.toLowerCase() == y.toLowerCase() : x == y;
|
|
}
|
|
|
|
/// 79 · 87: 찾기 · 분석 결과 줄의 폴더 - 시작한 폴더 이름부터의 상대 경로 ("Videos\Show\S1").
|
|
/// 단계가 [keep] + 1 보다 많으면 가운데를 줄인다 ("Videos\…\S1\E01"). 시작한 폴더 밖이면 전체 경로.
|
|
/// (15 캡처: 폭 412 에서 절대 경로의 앞부분만 보여 구별에 필요한 아래 폴더가 잘림)
|
|
String shortUnder(String root, String dir, {int keep = 2}) {
|
|
final parts = <String>[];
|
|
var x = dir;
|
|
for (var i = 0; i < 256 && !samePath(x, root); i++) {
|
|
final up = vDirname(x);
|
|
if (up == x) return vDisplay(dir);
|
|
parts.insert(0, vBasename(x));
|
|
x = up;
|
|
}
|
|
if (!samePath(x, root)) return vDisplay(dir);
|
|
// 드라이브 · 서버 맨 위는 이름이 없어 그대로 (C: · ☁ 서버)
|
|
final top = vDirname(root) == root;
|
|
parts.insert(0, top ? (isDav(root) ? vDisplay(root) : root.replaceAll(RegExp(r'[\\/]+$'), '')) : vBasename(root));
|
|
final sep = isDav(dir) ? '/' : p.separator;
|
|
if (parts.length > keep + 1) return [parts.first, '…', ...parts.sublist(parts.length - keep)].join(sep);
|
|
return parts.join(sep);
|
|
}
|
|
|
|
/// [a] 가 [b] 와 같거나 그 안에 있는지 (폴더를 자기 안으로 복사 · 이동하지 않게)
|
|
bool isSameOrInside(String a, String b) {
|
|
final dav = davInside(a, b);
|
|
if (dav != null) return dav;
|
|
final x = p.normalize(p.absolute(a)), y = p.normalize(p.absolute(b));
|
|
return samePath(a, b) || p.isWithin(y, x) || (Platform.isWindows && p.isWithin(y.toLowerCase(), x.toLowerCase()));
|
|
}
|
|
|
|
/// 79-c: 고른 것 중 다른 고른 폴더 안에 있는 것 · 같은 것을 빼고 바깥 것만 (차례는 그대로).
|
|
/// 폴더와 그 안의 파일을 함께 고르면 안의 파일을 두 번 처리하거나 (이동 뒤 "없음" 오류) 확인 창 개수가 부풀었다
|
|
List<String> outermostPaths(Iterable<String> paths) {
|
|
final all = paths.toList();
|
|
final out = <String>[];
|
|
for (var i = 0; i < all.length; i++) {
|
|
final x = all[i];
|
|
if (out.any((o) => samePath(o, x))) continue;
|
|
if (all.any((o) => !samePath(o, x) && isSameOrInside(x, o))) continue;
|
|
out.add(x);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/// 복사 · 이동을 시작하기 전 확인 (모든 방법 공통 - rsync · robocopy 는 스스로 막지 않는다).
|
|
/// 문제가 있으면 알릴 글, 없으면 null.
|
|
/// - 원본이 없음 (다른 곳에서 지워졌거나 옮겨짐) · 대상 폴더가 없음
|
|
/// - 폴더를 자기 자신 (또는 그 안) 으로 복사 · 이동
|
|
/// - 이미 그 폴더에 있는 것을 같은 폴더로 이동
|
|
/// [contents]: 폴더 "안의 것" 을 옮긴다 (Rsync 화면 · 원본/ → 대상/) - 바로 위 폴더로 옮기는 것도 된다 (205)
|
|
String? transferProblem(List<String> sources, String dest, {required bool move, bool contents = false}) {
|
|
// WebDAV 경로는 네트워크라 여기서 있는지 확인하지 않는다 (없으면 복사할 때 알림)
|
|
final missing = [for (final s in sources) if (vMissingSync(s)) s];
|
|
if (missing.isNotEmpty) {
|
|
return trf('원본이 없습니다 (다른 곳에서 지워졌거나 옮겨졌습니다): {0}', [missing.map(vBasename).join(', ')]);
|
|
}
|
|
if (!isDav(dest) && !Directory(dest).existsSync()) return trf('대상 폴더가 없습니다: {0}', [dest]);
|
|
for (final s in sources) {
|
|
if ((isDav(s) || FileSystemEntity.isDirectorySync(s)) && isSameOrInside(dest, s)) {
|
|
return trf('폴더를 자기 자신 안으로 {0} 수 없습니다: {1}', [move ? tr('옮길') : tr('복사할'), vBasename(s)]);
|
|
}
|
|
// 폴더째 옮길 때만: 이미 그 폴더 안에 있으면 옮길 것이 없다.
|
|
// 205: 안의 것을 바로 위 폴더로 올리는 이동 (Down\Movie/ → Down) 은 막지 않는다 (예전에는 조용히 시작하지 않았다)
|
|
if (move && !contents && samePath(vDirname(s), dest)) return trf('이미 이 폴더에 있습니다: {0}', [vBasename(s)]);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// 복사 · 동기화가 다 쓴 뒤 이름을 바꾸기 전의 임시 파일 (끊기면 남는다 - 71)
|
|
bool isPartialFile(String name) => name.endsWith('.jjpart') || name.endsWith('.jjsync');
|
|
|
|
/// 78 보충 (관리자 판단): 일괄 이름 바꾸기가 잠깐 쓰는 임시 이름 (~jjren_ · ~jjback_) - 실시간 동기화 · 맞추기 · rsync 에 섞이지 않게
|
|
bool isAppTempName(String name) => name.startsWith('~jjren_') || name.startsWith('~jjback_');
|
|
|
|
/// rsync · robocopy 에 넣을 임시 이름 제외 (앱이 만드는 인수)
|
|
const appTempRsyncExcludes = ['--exclude=~jjren_*', '--exclude=~jjback_*'];
|
|
const appTempRobocopyExcludes = ['/XF', '~jjren_*', '~jjback_*'];
|