E10 · 50 보충: 로컬 폴더를 읽지 못하면 빈 폴더로 보이지 않게 ("읽을 수 없습니다 · 이유" 와 [다시 시도])

- listEntries 가 Directory.list 의 오류를 버려 권한 없는 폴더 · 빠진 USB · 없는 폴더가 빈 폴더로 보였다.
  하나도 읽지 못했으면 그 오류를 던지고, 일부를 읽은 뒤의 오류는 읽은 것까지 보여 준다
- 부르는 다른 곳: [폴더 재생] 은 이유 알림, [MKV 목록에 추가] 는 읽지 못한 폴더를 건너뛰고 알림, 그림 보기는 고른 그림만
- 시험: 빈 폴더 · 없는 폴더 · C:\System Volume Information (권한 없음) - 탐색기에 "이 폴더를 읽을 권한이 없습니다" 와 [다시 시도]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음3
RyuJongJin 2026-10-09 20:52:02 +09:00
parent c25a754035
commit ba332f9f84
7 changed files with 56 additions and 5 deletions

View File

@ -1400,6 +1400,8 @@
"일시정지": "Pause", "일시정지": "Pause",
"읽기 전용 매체입니다": "Read-only media", "읽기 전용 매체입니다": "Read-only media",
"읽을 수 없습니다": "Cannot read", "읽을 수 없습니다": "Cannot read",
"읽을 수 없습니다: {0}": "Cannot read: {0}",
"읽을 수 없어 건너뛴 폴더: {0}": "Skipped folders that could not be read: {0}",
"읽을 수 없음": "Cannot read", "읽을 수 없음": "Cannot read",
"읽지 못한 원래 파일은 지우지 않고 아래에 보관했습니다:": "The original file that could not be read was not deleted; it is kept here:", "읽지 못한 원래 파일은 지우지 않고 아래에 보관했습니다:": "The original file that could not be read was not deleted; it is kept here:",
"읽지 못한 폴더 {0}개 (권한 · 연결 문제) - 그 안은 찾지 못했습니다": "{0} unreadable folders (permission or connection problem) - not searched inside", "읽지 못한 폴더 {0}개 (권한 · 연결 문제) - 그 안은 찾지 못했습니다": "{0} unreadable folders (permission or connection problem) - not searched inside",

View File

@ -1400,6 +1400,8 @@
"일시정지": "一時停止", "일시정지": "一時停止",
"읽기 전용 매체입니다": "読み取り専用メディアです", "읽기 전용 매체입니다": "読み取り専用メディアです",
"읽을 수 없습니다": "読み込めません", "읽을 수 없습니다": "読み込めません",
"읽을 수 없습니다: {0}": "読み込めません: {0}",
"읽을 수 없어 건너뛴 폴더: {0}": "読み込めずスキップしたフォルダー: {0}",
"읽을 수 없음": "読み取れません", "읽을 수 없음": "読み取れません",
"읽지 못한 원래 파일은 지우지 않고 아래에 보관했습니다:": "読めなかった元のファイルは削除せず、ここに保管しました:", "읽지 못한 원래 파일은 지우지 않고 아래에 보관했습니다:": "読めなかった元のファイルは削除せず、ここに保管しました:",
"읽지 못한 폴더 {0}개 (권한 · 연결 문제) - 그 안은 찾지 못했습니다": "読めなかったフォルダー {0} 個 (権限・接続の問題) - その中は探せませんでした", "읽지 못한 폴더 {0}개 (권한 · 연결 문제) - 그 안은 찾지 못했습니다": "読めなかったフォルダー {0} 個 (権限・接続の問題) - その中は探せませんでした",

View File

@ -1400,6 +1400,8 @@
"일시정지": "暂停", "일시정지": "暂停",
"읽기 전용 매체입니다": "只读介质", "읽기 전용 매체입니다": "只读介质",
"읽을 수 없습니다": "无法读取", "읽을 수 없습니다": "无法读取",
"읽을 수 없습니다: {0}": "无法读取: {0}",
"읽을 수 없어 건너뛴 폴더: {0}": "无法读取而跳过的文件夹: {0}",
"읽을 수 없음": "无法读取", "읽을 수 없음": "无法读取",
"읽지 못한 원래 파일은 지우지 않고 아래에 보관했습니다:": "无法读取的原文件没有删除,保存在这里:", "읽지 못한 원래 파일은 지우지 않고 아래에 보관했습니다:": "无法读取的原文件没有删除,保存在这里:",
"읽지 못한 폴더 {0}개 (권한 · 연결 문제) - 그 안은 찾지 못했습니다": "{0} 个无法读取的文件夹 (权限 · 连接问题) - 未在其中查找", "읽지 못한 폴더 {0}개 (권한 · 연결 문제) - 그 안은 찾지 못했습니다": "{0} 个无法读取的文件夹 (权限 · 连接问题) - 未在其中查找",

View File

@ -57,7 +57,10 @@ Future<List<FileEntry>> listEntries(String dir, {bool showHidden = false}) async
]; ];
} }
final out = <FileEntry>[]; final out = <FileEntry>[];
await for (final e in Directory(dir).list(followLinks: false).handleError((_) {})) { // E10: 폴더를 읽지 못하면 (권한 없음 · 빠진 USB 등) 빈 폴더로 보이지 않게 던진다 (화면이 "읽을 수 없습니다: 이유" 와 [다시 시도]).
// 일부를 읽은 뒤의 오류는 읽은 것까지 보여 준다
Object? failed;
await for (final e in Directory(dir).list(followLinks: false).handleError((Object err) => failed ??= err)) {
try { try {
final st = await e.stat(); final st = await e.stat();
final isDir = st.type == FileSystemEntityType.directory; final isDir = st.type == FileSystemEntityType.directory;
@ -66,6 +69,7 @@ Future<List<FileEntry>> listEntries(String dir, {bool showHidden = false}) async
out.add(entry); out.add(entry);
} catch (_) {} } catch (_) {}
} }
if (failed != null && out.isEmpty) throw failed!;
return out; return out;
} }

View File

@ -909,7 +909,13 @@ class _ExplorerPageState extends State<ExplorerPage> {
/// 그림 보기: 같은 폴더의 그림을 지금 정렬 순서대로 넘겨 본다 /// 그림 보기: 같은 폴더의 그림을 지금 정렬 순서대로 넘겨 본다
Future<void> _viewImages(String path, _Pane? pane) async { Future<void> _viewImages(String path, _Pane? pane) async {
final dir = vDirname(path); final dir = vDirname(path);
final list = pane?.cache[dir] ?? _sorted(await listEntries(dir, showHidden: c.settings.explorerShowHidden)); // 폴더를 읽지 못하면 (E10: 이제 오류로 알림) 고른 그림 하나만
List<FileEntry> list;
try {
list = pane?.cache[dir] ?? _sorted(await listEntries(dir, showHidden: c.settings.explorerShowHidden));
} catch (_) {
list = const [];
}
final images = [ final images = [
for (final e in list) for (final e in list)
if (!e.isDir && c.settings.imageExts.contains(extOf(e.path))) e.path, if (!e.isDir && c.settings.imageExts.contains(extOf(e.path))) e.path,
@ -1252,7 +1258,13 @@ class _ExplorerPageState extends State<ExplorerPage> {
} }
Future<void> _playFolder(String dir) async { Future<void> _playFolder(String dir) async {
final list = (await listEntries(dir)).where((e) => !e.isDir && isVideoFile(e.path)).map((e) => e.path).toList(); final List<String> list;
try {
list = (await listEntries(dir)).where((e) => !e.isDir && isVideoFile(e.path)).map((e) => e.path).toList();
} catch (e) {
_snack(trf('읽을 수 없습니다: {0}', [explainFileError('$e', dav: isDav(dir)).$1]));
return;
}
if (list.isEmpty) { if (list.isEmpty) {
_snack(tr('이 폴더에 동영상이 없습니다.')); _snack(tr('이 폴더에 동영상이 없습니다.'));
return; return;
@ -1267,15 +1279,21 @@ class _ExplorerPageState extends State<ExplorerPage> {
return; return;
} }
final videos = <String>[]; final videos = <String>[];
final unreadable = <String>[];
for (final x in paths) { for (final x in paths) {
if (vIsDirSync(x)) { if (vIsDirSync(x)) {
videos.addAll((await listEntries(x)).where((e) => !e.isDir && isVideoFile(e.path)).map((e) => e.path)); try {
videos.addAll((await listEntries(x)).where((e) => !e.isDir && isVideoFile(e.path)).map((e) => e.path));
} catch (_) {
unreadable.add(vBasename(x)); // 읽지 못한 폴더는 건너뛰고 알린다
}
} else if (isVideoFile(x)) { } else if (isVideoFile(x)) {
videos.add(x); videos.add(x);
} }
} }
if (unreadable.isNotEmpty) _snack(trf('읽을 수 없어 건너뛴 폴더: {0}', [unreadable.take(3).join(', ')]));
if (videos.isEmpty) { if (videos.isEmpty) {
_snack(tr('추가할 동영상이 없습니다.')); if (unreadable.isEmpty) _snack(tr('추가할 동영상이 없습니다.'));
return; return;
} }
await c.addVideos(videos, allowOutputFolder: true); await c.addVideos(videos, allowOutputFolder: true);

View File

@ -243,6 +243,20 @@ void main() {
expect(find.text('앞 화면'), findsOneWidget); expect(find.text('앞 화면'), findsOneWidget);
}); });
testWidgets('E10: 권한 없는 폴더는 빈 폴더가 아니라 "읽을 수 없음 · 이유" 와 [다시 시도]', (tester) async {
const locked = r'C:\System Volume Information';
if (!Platform.isWindows || !Directory(locked).existsSync()) return;
c.settings.explorerPaths = [locked, right];
ExplorerPage.debugVolumes = () => [(r'C:\', 'C:'), (p.rootPrefix(tmp.path), p.rootPrefix(tmp.path).substring(0, 2))];
tester.view.physicalSize = const Size(1600, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
await settle(tester, () => find.textContaining('이 폴더를 읽을 권한이 없습니다').evaluate().isNotEmpty);
expect(find.textContaining('이 폴더를 읽을 권한이 없습니다'), findsOneWidget);
expect(find.text('다시 시도'), findsOneWidget);
});
testWidgets('48: 건너뛰기를 고르면 그대로 두고 건너뛴 것을 알린다', (tester) async { testWidgets('48: 건너뛰기를 고르면 그대로 두고 건너뛴 것을 알린다', (tester) async {
await startCopy(tester); await startCopy(tester);
await tester.tap(find.text('건너뛰기')); await tester.tap(find.text('건너뛰기'));

View File

@ -200,4 +200,13 @@ void main() {
expect(File(p.join(b.path, 'k')).readAsStringSync(), 'file'); expect(File(p.join(b.path, 'k')).readAsStringSync(), 'file');
}); });
}); });
test('E10: 폴더를 읽지 못하면 빈 목록이 아니라 오류 (권한 없음 · 없는 폴더) · 빈 폴더는 그대로 빈 목록', () async {
final empty = Directory(p.join(tmp.path, 'empty'))..createSync();
expect(await listEntries(empty.path), isEmpty);
await expectLater(listEntries(p.join(tmp.path, 'gone')), throwsA(isA<FileSystemException>()));
if (Platform.isWindows && Directory(r'C:\System Volume Information').existsSync()) {
await expectLater(listEntries(r'C:\System Volume Information'), throwsA(isA<FileSystemException>()));
}
});
} }