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 = []; 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 outermostPaths(Iterable paths) { final all = paths.toList(); final out = []; 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 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_*'];