import 'dart:io'; import 'package:path/path.dart' as p; import '../../l10n/tr.dart'; import '../vfs.dart'; // 약속 (나누기 file_ops): 이름 규칙과 이름 일 - 쓸 수 없는 이름 · 이름 바꾸기 (같은 폴더 안) · 새 폴더. // 입력: 경로 · 새 이름 (앞뒤 빈칸은 뺌). 출력: 문제 글 (괜찮으면 null) · 새 경로. // 오류: 쓸 수 없는 이름 · 같은 이름이 이미 있음 → [FileSystemException] (그 글은 화면에 그대로 보인다). 저장소 오류는 그대로. /// 78 보충 (관리자 판단): 이름으로 쓸 수 없는 까닭 (괜찮으면 null). /// 모든 곳: 빈 이름 · . · .. · \ / : * ? " < > | · 제어 문자. /// [windowsRules] (Windows 의 로컬 경로): CON · PRN · AUX · NUL · COM1~9 · LPT1~9 (확장자가 붙어도), 끝이 점 · 빈칸, 255자 넘음 String? nameProblem(String name, {required bool windowsRules}) { if (name.isEmpty || name == '.' || name == '..' || name.contains(RegExp(r'[\\/:*?"<>|\x00-\x1f]'))) return tr('쓸 수 없는 이름'); if (!windowsRules) return null; final base = name.split('.').first.trim().toUpperCase(); if (RegExp(r'^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$').hasMatch(base)) return trf('Windows 가 쓰는 이름 ({0})', [base]); if (name.endsWith('.') || name.endsWith(' ')) return tr('이름 끝에 점 · 빈칸은 쓸 수 없음'); if (name.length > 255) return tr('이름이 너무 김 (255자 넘음)'); return null; } /// 이 경로에 Windows 이름 규칙을 쓰는지 (Windows 의 로컬 · 네트워크 공유. WebDAV 는 서버 규칙이라 빼고) bool usesWindowsNames(String path) => Platform.isWindows && !isDav(path); /// 이름 바꾸기 (같은 폴더 안). 새 경로를 돌려준다. Future renameInPlace(String path, String newName) async { final name = newName.trim(); final bad = nameProblem(name, windowsRules: usesWindowsNames(path)); if (bad != null) throw FileSystemException(bad, name); if (isDav(path)) { final target = vJoin(vDirname(path), name); if (target == vNorm(path)) return target; if (await vExists(target)) throw FileSystemException(tr('같은 이름이 이미 있습니다'), target); await vRename(path, target); return target; } final target = p.join(p.dirname(path), name); if (target == path) return path; if (FileSystemEntity.typeSync(target) != FileSystemEntityType.notFound && (target.toLowerCase() != path.toLowerCase() || _otherCaseExists(path, name))) { throw FileSystemException(tr('같은 이름이 이미 있습니다'), target); } final t = FileSystemEntity.typeSync(path); return t == FileSystemEntityType.directory ? (await Directory(path).rename(target)).path : (await File(path).rename(target)).path; } /// 데이터 검토 (10/18): 대소문자만 바꾸는 이름 (a.jpg → A.jpg) 인데 대소문자를 가리는 저장소 (Android 의 앱 폴더 · ext4 · 일부 NAS) 에 /// 그 이름의 다른 파일이 따로 있으면 true - rename 이 그 파일을 말없이 덮지 않게 (가리지 않는 곳이면 목록에 정확히 그 이름이 없다) bool _otherCaseExists(String path, String name) { if (p.basename(path) == name) return false; try { return Directory(p.dirname(path)).listSync(followLinks: false).any((e) => p.basename(e.path) == name); } catch (_) { return true; // 모르면 막는다 (덮는 것보다 낫다) } } /// [dir] 안에 새 폴더 [name]. 만든 경로를 돌려준다. Future createFolder(String dir, String name) async { final n = name.trim(); final bad = nameProblem(n, windowsRules: usesWindowsNames(dir)); if (bad != null) throw FileSystemException(bad, n); if (isDav(dir)) { final target = vJoin(dir, n); if (await vExists(target)) throw FileSystemException(tr('같은 이름이 이미 있습니다'), target); await vMkdirs(target); return target; } final target = p.join(dir, n); if (FileSystemEntity.typeSync(target) != FileSystemEntityType.notFound) { throw FileSystemException(tr('같은 이름이 이미 있습니다'), target); } return (await Directory(target).create()).path; }