From f97f63df79fb4f6a7c2a9fc679c1c717cc5ce314 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:31:07 +0900 Subject: [PATCH] =?UTF-8?q?144=20=EA=B8=B4=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=A7=80=EC=9A=B0=EA=B8=B0:=20=ED=9C=B4=EC=A7=80=ED=86=B5?= =?UTF-8?q?=EC=97=90=20=EB=84=A3=EC=A7=80=20=EB=AA=BB=ED=95=9C=20=EC=9D=B4?= =?UTF-8?q?=EC=9C=A0=EB=8A=94=20=ED=95=9C=20=EC=A4=84=20=C2=B7=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EB=A7=8C=20=C2=B7=20[=EA=B7=B8=EB=8C=80=EB=A1=9C=20?= =?UTF-8?q?=EB=91=90=EA=B8=B0]=20/=20[=EC=98=81=EA=B5=AC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20(n=EA=B0=9C,=20=EB=90=98=EB=8F=8C=EB=A6=B4=20?= =?UTF-8?q?=EC=88=98=20=EC=97=86=EC=9D=8C)]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 휴지통이 받지 않는 긴 경로 (260자 넘음) 는 LongPathException 으로 따로 모아, 실패 창 위에 이유 한 줄과 이름만 - [영구 삭제] 는 그 항목만 지우고 결과를 알림. 긴 경로가 없으면 예전처럼 [확인] - 시험: 긴 경로 폴더 → 이유 한 줄 · [그대로 두기] 면 남음 · [영구 삭제] 면 지움 - recycle_bin_test: 148 의 "원래 폴더 없음" 시험이 휴지통에 남긴 것을 정확히 그 폴더로 찾아 지우게 (사용자 휴지통에 남던 것) Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 5 +++ assets/l10n/ja.json | 5 +++ assets/l10n/zh-Hans.json | 5 +++ lib/platform/windows/recycle_bin.dart | 7 +++- lib/ui/explorer_page.dart | 48 ++++++++++++++++++++++++--- test/explorer_conflict_test.dart | 38 +++++++++++++++++++++ test/recycle_bin_test.dart | 5 +-- 7 files changed, 105 insertions(+), 8 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 6d3c566..65bef95 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -333,6 +333,7 @@ "{0}개는 휴지통보다 커서 영구 삭제했습니다 (Windows 가 물은 대로).": "{0} were too large for the Recycle Bin and were deleted permanently (as Windows asked).", "{0}개는 휴지통에 들어가지 않아 영구 삭제했습니다 (Windows 가 묻고 고른 대로).": "{0} didn't fit in the Recycle Bin and were deleted permanently (as chosen when Windows asked).", "{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "Imported {0} into \"Other bookmarks\".", + "{0}개를 영구 삭제했습니다.": "Permanently deleted {0} item(s).", "{0}개를 옮겼습니다 → {1}": "Moved {0} → {1}", "{0}개를 옮겼습니다 → {1} ({2})": "Moved {0} → {1} ({2})", "{0}개를 찾아 여기서 멈췄습니다. 이름을 더 자세히 넣거나 아래 폴더로 들어가 찾으세요.": "Stopped after finding {0}. Enter a more specific name or search inside a subfolder.", @@ -406,6 +407,7 @@ "경로": "Path", "경로 입력": "Enter path", "경로 입력 (예: D:\\영상 · \\\\NAS\\공유)": "Enter path (e.g. D:\\Videos · \\\\NAS\\share)", + "경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "The path is too long for the Recycle Bin (the Windows Recycle Bin does not accept paths over 260 characters).", "경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "The path is too long (the Windows Recycle Bin doesn't accept paths over 260 characters) - you can delete it permanently", "계속": "Always", "계속 (앱이 켜져 있는 동안)": "Always (while the app is running)", @@ -434,6 +436,7 @@ "권한 허용 화면 열기": "Open the permission screen", "권한이 없습니다": "No permission", "그 파일 하나만": "Only that file", + "그대로 두기": "Keep", "그래도 맞추기 ({0}개 지움)": "Sync anyway (delete {0})", "그리는 중 {0}/{1}": "Drawing {0}/{1}", "그리려면 {0} 를 받아야 합니다 (모델은 받은 뒤 이 기기에만 있고, 그림 · 프롬프트는 밖으로 나가지 않습니다).": "To draw, {0} must be downloaded (the model stays on this device, and images and prompts never leave it).", @@ -1153,6 +1156,7 @@ "열 수 없습니다: {0}": "Cannot open: {0}", "열기": "Open", "영구 삭제": "Delete permanently", + "영구 삭제 ({0}개, 되돌릴 수 없음)": "Delete permanently ({0}, cannot be undone)", "영상 {0}": "Video {0}", "영상 · 음성 합치는 중": "Merging video · audio", "영상 재생 오류: {0}": "Video playback error: {0}", @@ -1601,6 +1605,7 @@ "지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.": "This run deletes files, so you can run it after the list of deletions appears.", "지우면 휴지통으로": "Delete to the Recycle Bin", "지우지 못한 파일: {0}": "Couldn't delete: {0}", + "지우지 못함: {0}": "Could not delete: {0}", "지우지 못했습니다: {0}": "Could not delete: {0}", "지우지 않고 맞춤 · 대상에만 있는 {0}개를 지울지 확인 필요": "Synced without deleting · confirm whether to delete {0} items only in the target", "지우지 않기 (지우기 끄기)": "Don't delete (turn off delete)", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index f0818cf..44123cd 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -333,6 +333,7 @@ "{0}개는 휴지통보다 커서 영구 삭제했습니다 (Windows 가 물은 대로).": "{0} 個はごみ箱より大きいため完全に削除しました (Windows の確認どおり)。", "{0}개는 휴지통에 들어가지 않아 영구 삭제했습니다 (Windows 가 묻고 고른 대로).": "{0} 件はごみ箱に入らなかったため完全に削除しました (Windows の確認で選んだとおり)。", "{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "{0}個を「その他のブックマーク」に取り込みました。", + "{0}개를 영구 삭제했습니다.": "{0} 件を完全に削除しました。", "{0}개를 옮겼습니다 → {1}": "{0}個を移動しました → {1}", "{0}개를 옮겼습니다 → {1} ({2})": "{0}個を移動しました → {1}({2})", "{0}개를 찾아 여기서 멈췄습니다. 이름을 더 자세히 넣거나 아래 폴더로 들어가 찾으세요.": "{0} 件見つけたところで止めました。名前をより詳しく入力するか、下のフォルダーに入って探してください。", @@ -406,6 +407,7 @@ "경로": "パス", "경로 입력": "パスを入力", "경로 입력 (예: D:\\영상 · \\\\NAS\\공유)": "パスを入力 (例: D:\\動画・\\\\NAS\\共有)", + "경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "パスが長すぎてごみ箱に入れられません (Windows のごみ箱は 260 文字を超えるパスを受け付けません)。", "경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "パスが長すぎます (Windows のごみ箱は 260 文字を超えるパスを受け付けません) - 完全に削除はできます", "계속": "常時", "계속 (앱이 켜져 있는 동안)": "常時 (アプリの起動中)", @@ -434,6 +436,7 @@ "권한 허용 화면 열기": "権限の許可画面を開く", "권한이 없습니다": "権限がありません", "그 파일 하나만": "そのファイルだけ", + "그대로 두기": "そのままにする", "그래도 맞추기 ({0}개 지움)": "それでも同期 ({0} 個を削除)", "그리는 중 {0}/{1}": "描画中 {0}/{1}", "그리려면 {0} 를 받아야 합니다 (모델은 받은 뒤 이 기기에만 있고, 그림 · 프롬프트는 밖으로 나가지 않습니다).": "描くには {0} のダウンロードが必要です (モデルはこの端末にだけ置かれ、画像・プロンプトは外に出ません)。", @@ -1153,6 +1156,7 @@ "열 수 없습니다: {0}": "開けません: {0}", "열기": "開く", "영구 삭제": "完全に削除", + "영구 삭제 ({0}개, 되돌릴 수 없음)": "完全に削除 ({0} 件、元に戻せません)", "영상 {0}": "映像 {0}", "영상 · 음성 합치는 중": "映像・音声を結合中", "영상 재생 오류: {0}": "動画再生エラー: {0}", @@ -1601,6 +1605,7 @@ "지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.": "削除を含む実行のため、削除一覧が表示されてから実行できます。", "지우면 휴지통으로": "削除するとごみ箱へ", "지우지 못한 파일: {0}": "削除できなかったファイル: {0}", + "지우지 못함: {0}": "削除できません: {0}", "지우지 못했습니다: {0}": "削除できませんでした: {0}", "지우지 않고 맞춤 · 대상에만 있는 {0}개를 지울지 확인 필요": "削除せずに同期・対象にだけある {0} 個を削除するか確認が必要", "지우지 않기 (지우기 끄기)": "削除しない (削除をオフ)", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 52484a4..5c6fddb 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -333,6 +333,7 @@ "{0}개는 휴지통보다 커서 영구 삭제했습니다 (Windows 가 물은 대로).": "{0} 个超出回收站容量,已永久删除 (按 Windows 的提示)。", "{0}개는 휴지통에 들어가지 않아 영구 삭제했습니다 (Windows 가 묻고 고른 대로).": "{0} 个项目无法放入回收站,已永久删除 (按 Windows 询问时的选择)。", "{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "已将 {0} 个导入“其他书签”。", + "{0}개를 영구 삭제했습니다.": "已永久删除 {0} 个项目。", "{0}개를 옮겼습니다 → {1}": "已移动 {0} 个 → {1}", "{0}개를 옮겼습니다 → {1} ({2})": "已移动 {0} 个 → {1}({2})", "{0}개를 찾아 여기서 멈췄습니다. 이름을 더 자세히 넣거나 아래 폴더로 들어가 찾으세요.": "找到 {0} 个后已停止。请输入更具体的名称或进入下级文件夹查找。", @@ -406,6 +407,7 @@ "경로": "路径", "경로 입력": "输入路径", "경로 입력 (예: D:\\영상 · \\\\NAS\\공유)": "输入路径 (例: D:\\视频 · \\\\NAS\\共享)", + "경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).": "路径过长,无法放入回收站 (Windows 回收站不接受超过 260 个字符的路径)。", "경로가 너무 깁니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다) - 영구 삭제로 지울 수 있습니다": "路径过长 (Windows 回收站不接受超过 260 个字符的路径) - 可以永久删除", "계속": "持续", "계속 (앱이 켜져 있는 동안)": "持续 (应用运行期间)", @@ -434,6 +436,7 @@ "권한 허용 화면 열기": "打开权限授予界面", "권한이 없습니다": "没有权限", "그 파일 하나만": "仅该文件", + "그대로 두기": "保留", "그래도 맞추기 ({0}개 지움)": "仍然同步 (删除 {0} 个)", "그리는 중 {0}/{1}": "正在绘制 {0}/{1}", "그리려면 {0} 를 받아야 합니다 (모델은 받은 뒤 이 기기에만 있고, 그림 · 프롬프트는 밖으로 나가지 않습니다).": "要绘制,需要下载 {0} (模型只保存在本设备上,图片和提示词不会离开设备)。", @@ -1153,6 +1156,7 @@ "열 수 없습니다: {0}": "无法打开: {0}", "열기": "打开", "영구 삭제": "永久删除", + "영구 삭제 ({0}개, 되돌릴 수 없음)": "永久删除 ({0} 个,无法撤销)", "영상 {0}": "视频 {0}", "영상 · 음성 합치는 중": "正在合并视频 · 音频", "영상 재생 오류: {0}": "视频播放错误: {0}", @@ -1601,6 +1605,7 @@ "지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.": "此操作包含删除,需在显示删除列表后才能执行。", "지우면 휴지통으로": "删除时移到回收站", "지우지 못한 파일: {0}": "无法删除的文件: {0}", + "지우지 못함: {0}": "无法删除: {0}", "지우지 못했습니다: {0}": "无法删除: {0}", "지우지 않고 맞춤 · 대상에만 있는 {0}개를 지울지 확인 필요": "已同步但未删除 · 需确认是否删除仅在目标中的 {0} 个项目", "지우지 않기 (지우기 끄기)": "不删除 (关闭删除)", diff --git a/lib/platform/windows/recycle_bin.dart b/lib/platform/windows/recycle_bin.dart index 00135d1..179905f 100644 --- a/lib/platform/windows/recycle_bin.dart +++ b/lib/platform/windows/recycle_bin.dart @@ -214,6 +214,11 @@ String recycleErrorText(int code) => switch (code) { _ => trf('Windows 오류 0x{0}', [code.toRadixString(16)]), }; +/// 144: 경로가 너무 길어 휴지통이 받지 않는 것 (영구 삭제는 사용자가 따로 고른다). [path] 는 그 긴 경로 +class LongPathException extends FileSystemException { + const LongPathException(super.message, super.path); +} + /// Windows 휴지통으로 보내기 (SHFileOperationW, FO_DELETE + FOF_ALLOWUNDO). 65: 탐색기의 기본 지우기. /// 하나씩 보내 어느 것이 실패했는지 알 수 있게 한다. 실패하면 [FileSystemException] (이유는 읽을 수 있는 말). /// 휴지통보다 커서 들어가지 않으면 Windows 가 묻게 하고 (FOF_WANTNUKEWARNING), 실제로 어떻게 됐는지 돌려준다 (94). @@ -223,7 +228,7 @@ RecycleResult moveToRecycleBin(String path) { // 휴지통은 260자 넘는 경로를 받지 않고, 그때 Windows 는 묻지 않고 영구 삭제해 버린다 (시험에서 확인). // 그래서 보내기 전에 (폴더면 안의 것까지) 긴 경로가 있으면 지우지 않고 이유를 알린다 - 영구 삭제는 사용자가 따로 고른다. final long = longPathInside(path); - if (long != null) throw FileSystemException(recycleErrorText(0x7C), long); + if (long != null) throw LongPathException(recycleErrorText(0x7C), long); final shell32 = DynamicLibrary.open('shell32.dll'); final op = shell32.lookupFunction), int Function(Pointer<_ShFileOp>)>('SHFileOperationW'); final before = _binCount(path); diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index c5f5f1e..d705861 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -1332,6 +1332,8 @@ class _ExplorerPageState extends State { if (ok != true) return; // 하나씩: 실패해도 나머지는 계속. 실제로 어떻게 됐는지 세어 그대로 알린다 (94) final failed = <(String, String)>[]; + // 144: 경로가 너무 길어 휴지통이 받지 않은 것 (이유는 한 번, [영구 삭제] 로 바로 지울 수 있게) + final tooLong = []; var recycled = 0, nuked = 0, kept = 0; // 148: 휴지통으로 보낸 것 (되돌리기용) - 정보 파일 시각은 초 단위라 조금 앞에서부터 final since = DateTime.now().subtract(const Duration(seconds: 2)); @@ -1351,18 +1353,29 @@ class _ExplorerPageState extends State { } else { await FileOps().delete([x]); } + } on LongPathException { + tooLong.add(x); } catch (e) { failed.add((x, e is FileSystemException ? (e.osError?.message ?? e.message) : '$e')); } } - if (failed.isNotEmpty && mounted) { - await showDialog( + if ((failed.isNotEmpty || tooLong.isNotEmpty) && mounted) { + final nukeLong = await showDialog( context: context, builder: (ctx) => AlertDialog( scrollable: true, icon: const Icon(Icons.error_outline, color: Colors.redAccent), - title: Text(trf('{0}개 중 {1}개를 지우지 못했습니다', [paths.length, failed.length])), + title: Text(trf('{0}개 중 {1}개를 지우지 못했습니다', [paths.length, failed.length + tooLong.length])), content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + // 144: 긴 경로는 이유 한 줄 + 이름만 (영구 삭제는 아래 버튼으로) + if (tooLong.isNotEmpty) ...[ + Text(tr('경로가 너무 길어 휴지통에 넣을 수 없습니다 (Windows 의 휴지통은 260자 넘는 경로를 받지 않습니다).'), + style: const TextStyle(fontWeight: FontWeight.w600)), + const SizedBox(height: 4), + for (final x in tooLong.take(30)) Text('· ${vBasename(x)}', style: const TextStyle(fontSize: 12)), + if (tooLong.length > 30) Text(trf('… 외 {0}개', [tooLong.length - 30])), + const SizedBox(height: 8), + ], for (final (x, why) in failed.take(30)) ...[ Text(vBasename(x), style: const TextStyle(fontWeight: FontWeight.w600)), Text('${vDirname(x)} · $why', style: const TextStyle(fontSize: 12, color: JjColors.textDim)), @@ -1371,11 +1384,36 @@ class _ExplorerPageState extends State { if (failed.length > 30) Text(trf('… 외 {0}개', [failed.length - 30])), const SizedBox(height: 6), // 이유를 이미 위에 적었으므로 짐작하는 말은 빼고, 실제로 지운 것이 있을 때만 "나머지는 지웠습니다" - if (failed.length < paths.length) Text(tr('나머지는 지웠습니다.'), style: const TextStyle(fontSize: 12)), + if (failed.length + tooLong.length < paths.length) Text(tr('나머지는 지웠습니다.'), style: const TextStyle(fontSize: 12)), ]), - actions: [FilledButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('확인')))], + actions: [ + if (tooLong.isNotEmpty) ...[ + TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('그대로 두기'))), + FilledButton( + style: FilledButton.styleFrom(backgroundColor: Colors.redAccent), + onPressed: () => Navigator.pop(ctx, true), + child: Text(trf('영구 삭제 ({0}개, 되돌릴 수 없음)', [tooLong.length])), + ), + ] else + FilledButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('확인'))), + ], ), ); + // 144: 사용자가 [영구 삭제] 를 고른 긴 경로만 지운다 + if (nukeLong == true) { + final notDeleted = []; + for (final x in tooLong) { + try { + await FileOps().delete([x]); + } catch (e) { + notDeleted.add('${vBasename(x)}: ${e is FileSystemException ? (e.osError?.message ?? e.message) : e}'); + } + } + _snack([ + if (tooLong.length > notDeleted.length) trf('{0}개를 영구 삭제했습니다.', [tooLong.length - notDeleted.length]), + if (notDeleted.isNotEmpty) trf('지우지 못함: {0}', [notDeleted.take(3).join(' · ')]), + ].join(' ')); + } } else if (!permanent && mounted) { final text = [ if (recycled > 0) trf('{0}개 항목을 휴지통으로 보냈습니다.', [recycled]), diff --git a/test/explorer_conflict_test.dart b/test/explorer_conflict_test.dart index 5a430e2..b549ba5 100644 --- a/test/explorer_conflict_test.dart +++ b/test/explorer_conflict_test.dart @@ -166,6 +166,44 @@ void main() { await tester.pumpAndSettle(); }, skip: !Platform.isWindows); + testWidgets('144: 긴 경로는 휴지통에 넣지 못한 이유 한 줄과 [영구 삭제] - 누르면 지운다 · [그대로 두기] 면 남는다', (tester) async { + String lp(String s) => r'\\?\' + s; + var deep = p.join(left, 'deep'); + while (deep.length < 250) { + deep = p.join(deep, 'd' * 30); + } + Directory(lp(deep)).createSync(recursive: true); + File(lp(p.join(deep, 'a_file_name_that_makes_it_longer.txt'))).writeAsStringSync('x'); + 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.text('deep').evaluate().isNotEmpty); + Future deleteDeep() async { + await tester.runAsync(() => tester.tap(find.text('deep').first)); + await settle(tester, () => false, rounds: 10); + await tester.runAsync(() => tester.tap(find.text('삭제').first)); + await tester.pumpAndSettle(); + await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '휴지통으로'))); + await settle(tester, () => find.textContaining('경로가 너무 길어').evaluate().isNotEmpty); + } + + await deleteDeep(); + // 이유는 한 줄, 항목은 이름만, 버튼은 [그대로 두기] · [영구 삭제] + expect(find.textContaining('경로가 너무 길어 휴지통에 넣을 수 없습니다'), findsOneWidget); + expect(find.text('· deep'), findsOneWidget); + expect(find.textContaining('영구 삭제로 지울 수 있습니다'), findsNothing, reason: '긴 설명을 항목마다 되풀이하지 않음'); + await tester.tap(find.text('그대로 두기')); + await tester.pumpAndSettle(); + expect(Directory(lp(deep)).existsSync(), isTrue); + // 다시 → [영구 삭제] + await deleteDeep(); + await tester.runAsync(() => tester.tap(find.textContaining('영구 삭제 (1개, 되돌릴 수 없음)'))); + await settle(tester, () => find.textContaining('영구 삭제했습니다').evaluate().isNotEmpty); + expect(Directory(p.join(left, 'deep')).existsSync(), isFalse); + expect(find.textContaining('1개를 영구 삭제했습니다.'), findsOneWidget); + }, skip: !Platform.isWindows); + testWidgets('48: 건너뛰기를 고르면 그대로 두고 건너뛴 것을 알린다', (tester) async { await startCopy(tester); await tester.tap(find.text('건너뛰기')); diff --git a/test/recycle_bin_test.dart b/test/recycle_bin_test.dart index d9cafaf..de47d4e 100644 --- a/test/recycle_bin_test.dart +++ b/test/recycle_bin_test.dart @@ -84,12 +84,13 @@ void main() { test('148: 원래 폴더가 그사이 지워졌으면 다시 만들지 않고 알린다 · 이 사용자 (SID) 의 휴지통에서만 찾는다', () async { expect(currentUserSid(), matches(RegExp(r'^S-1-5-'))); final tmp = Directory.systemTemp.createTempSync('jj_undo_'); + final folder = Directory(p.join(tmp.absolute.path, 'gone'))..createSync(); addTearDown(() async { - await _purgeFromRecycleBin(tmp.absolute.path); + // 휴지통에 남긴 x.txt 의 원래 위치는 gone 폴더 (정확히 그 폴더로 찾아 지운다) + expect(await _purgeFromRecycleBin(folder.path), 1); if (tmp.existsSync()) tmp.deleteSync(recursive: true); }); final since = DateTime.now().subtract(const Duration(seconds: 2)); - final folder = Directory(p.join(tmp.absolute.path, 'gone'))..createSync(); final f = File(p.join(folder.path, 'x.txt'))..writeAsStringSync('x'); expect(moveToRecycleBin(f.path), RecycleResult.recycled); folder.deleteSync(); // 사용자가 그 폴더를 지움