80 lines
4.1 KiB
Dart
80 lines
4.1 KiB
Dart
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<String> 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<String> 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;
|
|
}
|