탐색기: 이동해도 그 폴더를 고르지 않음 · 148 휴지통으로 보낸 뒤 [되돌리기]

- 드라이브 탭 · 경로로 가기 · 상위 폴더 · .. · [열기] 로 가도 그 폴더를 복사 · 이동 대상으로 잡지 않는다
  (누르거나 표시하지 않은 폴더가 대상이 되지 않게, 위치는 지금 폴더 표시 · 스크롤로)
- 148: 휴지통으로 보낸 알림 (10초) 에 [되돌리기]. 이 사용자 (SID) 의 휴지통에서 보낸 시각 뒤의 $I 를 찾아 $R 을 원래 경로로.
  원래 자리에 같은 이름이 있거나 원래 폴더가 그사이 지워졌으면 되돌리지 않고 알림 (폴더를 다시 만들지 않음)
- 시험: 실제 휴지통으로 되돌리기 · 같은 이름 · 원래 폴더 없음 · 휴지통에 없음, 탐색기 [휴지통으로] → [되돌리기], 상위 폴더 뒤 [복사]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음3
RyuJongJin 2026-10-09 19:18:28 +09:00
parent 8f5d2204f0
commit e1e3316d8d
7 changed files with 213 additions and 5 deletions

View File

@ -325,6 +325,7 @@
"{0}개 항목을 {1}": "{0} items {1}",
"{0}개 항목을 다음 폴더로 {1}\n{2}": "{0} items to this folder — {1}\n{2}",
"{0}개 항목을 담았습니다. 넣을 폴더에서 [붙여넣기] 를 누르세요 ({1}).": "{0} items held. Press [Paste] in the destination folder ({1}).",
"{0}개 항목을 되돌렸습니다.": "Restored {0} item(s).",
"{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "Delete {0} items? This cannot be undone.",
"{0}개 항목을 휴지통으로 보낼까요?": "Move {0} items to the Recycle Bin?",
"{0}개 항목을 휴지통으로 보냈습니다.": "Moved {0} items to the Recycle Bin.",
@ -672,8 +673,10 @@
"동영상이 있는 폴더 아래 jj_mkv (기본)": "jj_mkv under the video's folder (default)",
"동의하고 받기": "Agree and download",
"동작 중 · {0} 까지": "Running · until {0}",
"되돌리기": "Undo",
"되돌리기 계속": "Continue rollback",
"되돌리기: AI 모델 {0} 을 복사해 두었습니다: {1}": "Rollback: copied AI models ({0}): {1}",
"되돌리지 못함: {0}": "Could not restore: {0}",
"되돌릴 수 없습니다.": "This can't be undone.",
"되살리고 다시 시작": "Restore and restart",
"되살리기": "Restore",
@ -1206,6 +1209,8 @@
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "Too many requests. Try again in a moment.",
"우르두어": "Urdu",
"우크라이나어": "Ukrainian",
"원래 자리에 같은 이름이 이미 있습니다": "An item with the same name already exists in the original place",
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "The original folder no longer exists, so it was not restored (restore it from the Recycle Bin yourself)",
"원문 보기 (번역 끄기)": "Show original (turn off translation)",
"원본": "Source",
"원본 비율": "Original aspect",
@ -1865,9 +1870,11 @@
"회전 없음": "No rotation",
"회차": "Episode",
"휴대폰 · 태블릿은 AV1 · VP9 을 하드웨어로 풀지 못해 끊기거나 배터리를 많이 쓰는 일이 있습니다. YouTube 의 H.264 는 1080p 까지라 더 큰 화질을 고르면 크기가 먼저입니다.": "Phones and tablets often can't decode AV1/VP9 in hardware, causing stutter or heavy battery use. YouTube's H.264 goes up to 1080p, so for higher qualities size comes first.",
"휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)": "Not found in the Recycle Bin (already emptied or restored)",
"휴지통으로": "To Recycle Bin",
"휴지통으로 보내기": "Move to Recycle Bin",
"휴지통을 거치지 않고 영구 삭제 (Shift+Delete)": "Delete permanently without the Recycle Bin (Shift+Delete)",
"휴지통이 없는 곳입니다": "This location has no Recycle Bin",
"휴지통이 없습니다": "No Recycle Bin",
"히브리어": "Hebrew",
"힌디어": "Hindi"

View File

@ -325,6 +325,7 @@
"{0}개 항목을 {1}": "{0} 件の項目を{1}",
"{0}개 항목을 다음 폴더로 {1}\n{2}": "{0} 件の項目を次のフォルダーへ{1}\n{2}",
"{0}개 항목을 담았습니다. 넣을 폴더에서 [붙여넣기] 를 누르세요 ({1}).": "{0} 件を保持しました。入れるフォルダーで [貼り付け] を押してください ({1})。",
"{0}개 항목을 되돌렸습니다.": "{0} 件を元に戻しました。",
"{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "{0} 件の項目を削除しますか? 元に戻せません。",
"{0}개 항목을 휴지통으로 보낼까요?": "{0} 個の項目をごみ箱に移動しますか?",
"{0}개 항목을 휴지통으로 보냈습니다.": "{0} 個の項目をごみ箱に移動しました。",
@ -672,8 +673,10 @@
"동영상이 있는 폴더 아래 jj_mkv (기본)": "動画があるフォルダーの下の jj_mkv(既定)",
"동의하고 받기": "同意してダウンロード",
"동작 중 · {0} 까지": "動作中 · {0} まで",
"되돌리기": "元に戻す",
"되돌리기 계속": "戻す操作を続ける",
"되돌리기: AI 모델 {0} 을 복사해 두었습니다: {1}": "戻す: AI モデル {0} をコピーしました: {1}",
"되돌리지 못함: {0}": "元に戻せません: {0}",
"되돌릴 수 없습니다.": "元に戻せません。",
"되살리고 다시 시작": "復元して再起動",
"되살리기": "元に戻す",
@ -1206,6 +1209,8 @@
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "リクエストが多すぎます。しばらくしてからもう一度お試しください。",
"우르두어": "ウルドゥー語",
"우크라이나어": "ウクライナ語",
"원래 자리에 같은 이름이 이미 있습니다": "元の場所に同じ名前が既にあります",
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "元のフォルダーがないため元に戻せませんでした (ごみ箱から直接復元してください)",
"원문 보기 (번역 끄기)": "原文を表示 (翻訳をオフ)",
"원본": "元",
"원본 비율": "元の比率",
@ -1865,9 +1870,11 @@
"회전 없음": "回転なし",
"회차": "話数",
"휴대폰 · 태블릿은 AV1 · VP9 을 하드웨어로 풀지 못해 끊기거나 배터리를 많이 쓰는 일이 있습니다. YouTube 의 H.264 는 1080p 까지라 더 큰 화질을 고르면 크기가 먼저입니다.": "スマホ・タブレットは AV1・VP9 をハードウェアで再生できず、途切れたりバッテリーを多く使うことがあります。YouTube の H.264 は 1080p までなので、それより高い画質を選ぶとサイズが優先です。",
"휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)": "ごみ箱に見つかりません (空にしたか元に戻した)",
"휴지통으로": "ごみ箱へ",
"휴지통으로 보내기": "ごみ箱に移動",
"휴지통을 거치지 않고 영구 삭제 (Shift+Delete)": "ごみ箱を使わずに完全に削除 (Shift+Delete)",
"휴지통이 없는 곳입니다": "ごみ箱がない場所です",
"휴지통이 없습니다": "ごみ箱がありません",
"히브리어": "ヘブライ語",
"힌디어": "ヒンディー語"

View File

@ -325,6 +325,7 @@
"{0}개 항목을 {1}": "{0} 个项目{1}",
"{0}개 항목을 다음 폴더로 {1}\n{2}": "将 {0} 个项目到以下文件夹 — {1}\n{2}",
"{0}개 항목을 담았습니다. 넣을 폴더에서 [붙여넣기] 를 누르세요 ({1}).": "已暂存 {0} 个项目。请在目标文件夹中按 [粘贴] ({1})。",
"{0}개 항목을 되돌렸습니다.": "已恢复 {0} 个项目。",
"{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "删除 {0} 个项目吗? 此操作无法撤销。",
"{0}개 항목을 휴지통으로 보낼까요?": "将 {0} 个项目移到回收站吗?",
"{0}개 항목을 휴지통으로 보냈습니다.": "已将 {0} 个项目移到回收站。",
@ -672,8 +673,10 @@
"동영상이 있는 폴더 아래 jj_mkv (기본)": "视频所在文件夹下的 jj_mkv(默认)",
"동의하고 받기": "同意并下载",
"동작 중 · {0} 까지": "运行中 · 直到 {0}",
"되돌리기": "撤销",
"되돌리기 계속": "继续回退",
"되돌리기: AI 모델 {0} 을 복사해 두었습니다: {1}": "回退: 已复制 AI 模型 {0}: {1}",
"되돌리지 못함: {0}": "无法恢复: {0}",
"되돌릴 수 없습니다.": "无法撤销。",
"되살리고 다시 시작": "恢复并重新启动",
"되살리기": "恢复",
@ -1206,6 +1209,8 @@
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "请求过多。请稍后再试。",
"우르두어": "乌尔都语",
"우크라이나어": "乌克兰语",
"원래 자리에 같은 이름이 이미 있습니다": "原位置已存在同名项目",
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "原文件夹已不存在,未能恢复 (请在回收站中手动还原)",
"원문 보기 (번역 끄기)": "显示原文 (关闭翻译)",
"원본": "源",
"원본 비율": "原始比例",
@ -1865,9 +1870,11 @@
"회전 없음": "不旋转",
"회차": "集数",
"휴대폰 · 태블릿은 AV1 · VP9 을 하드웨어로 풀지 못해 끊기거나 배터리를 많이 쓰는 일이 있습니다. YouTube 의 H.264 는 1080p 까지라 더 큰 화질을 고르면 크기가 먼저입니다.": "手机 · 平板常无法硬件解码 AV1 · VP9,会卡顿或耗电。YouTube 的 H.264 最高 1080p,选择更高画质时以尺寸优先。",
"휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)": "在回收站中找不到 (已清空或已恢复)",
"휴지통으로": "移到回收站",
"휴지통으로 보내기": "移到回收站",
"휴지통을 거치지 않고 영구 삭제 (Shift+Delete)": "不经过回收站永久删除 (Shift+Delete)",
"휴지통이 없는 곳입니다": "此位置没有回收站",
"휴지통이 없습니다": "没有回收站",
"히브리어": "希伯来语",
"힌디어": "印地语"

View File

@ -2,6 +2,7 @@ import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:path/path.dart' as p;
import '../../l10n/tr.dart';
@ -112,6 +113,76 @@ bool? recycledInfoExists(String original, {required DateTime since}) {
return readable ? false : null;
}
/// 지금 사용자의 SID (예: S-1-5-21-…, 휴지통 폴더 이름). 모르면 null
String? currentUserSid() => _sid ??= () {
try {
// 전체 경로로 (PATH 에 Git 등의 다른 whoami 가 먼저 있을 수 있다)
final exe = '${Platform.environment['SystemRoot'] ?? r'C:\Windows'}\\System32\\whoami.exe';
final r = Process.runSync(exe, ['/user', '/fo', 'csv', '/nh']);
return RegExp(r'"(S-1-[0-9-]+)"').firstMatch('${r.stdout}')?.group(1);
} catch (_) {
return null;
}
}();
String? _sid;
/// 148: 방금 휴지통으로 보낸 [original] 을 원래 자리로 되돌린다 ([since] 뒤에 생긴 것 중 가장 새것).
/// 휴지통의 $R… (내용) 을 원래 경로로 옮기고 $I… (정보) 를 지운다. 원래 자리에 이미 무엇이 있으면 덮지 않고 실패.
/// 실패하면 [FileSystemException] (이유는 읽을 수 있는 말).
void restoreFromRecycleBin(String original, {required DateTime since}) {
final m = RegExp(r'^([a-zA-Z]):').firstMatch(original);
if (m == null) throw FileSystemException(tr('휴지통이 없는 곳입니다'), original);
if (FileSystemEntity.typeSync(original, followLinks: false) != FileSystemEntityType.notFound) {
throw FileSystemException(tr('원래 자리에 같은 이름이 이미 있습니다'), original);
}
// 원래 폴더가 그사이 지워졌으면 다시 만들지 않는다 (사용자가 지운 폴더를 몰래 되살리지 않게)
if (!Directory(p.dirname(original)).existsSync()) {
throw FileSystemException(tr('원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)'), original);
}
// 이 사용자 (SID) 의 휴지통 폴더에서만 찾는다 (관리자 권한으로 다른 사용자의 휴지통을 읽을 수 있어도)
final sid = currentUserSid();
if (sid == null) throw FileSystemException(tr('휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)'), original);
File? info;
DateTime? newest;
final bin = Directory('${m[1]}:\\\$Recycle.Bin');
try {
for (final user in [Directory('${bin.path}\\$sid')]) {
List<FileSystemEntity> items;
try {
items = user.listSync(followLinks: false);
} catch (_) {
continue;
}
for (final f in items.whereType<File>()) {
if (!f.uri.pathSegments.last.startsWith(r'$I')) continue;
try {
final t = f.lastModifiedSync();
if (t.isBefore(since) || (newest != null && t.isBefore(newest))) continue;
final was = recycledInfoPath(f.readAsBytesSync());
if (was != null && was.toLowerCase() == original.toLowerCase()) {
info = f;
newest = t;
}
} catch (_) {}
}
}
} catch (_) {}
final i = info;
if (i == null) throw FileSystemException(tr('휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)'), original);
final dir = i.parent.path;
final content = '$dir\\\$R${i.uri.pathSegments.last.substring(2)}';
final type = FileSystemEntity.typeSync(content, followLinks: false);
if (type == FileSystemEntityType.notFound) throw FileSystemException(tr('휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)'), original);
if (type == FileSystemEntityType.directory) {
Directory(content).renameSync(original);
} else {
File(content).renameSync(original);
}
try {
i.deleteSync();
} catch (_) {}
}
/// 휴지통이 받을 수 있는 경로 길이 (MAX_PATH - 끝의 NUL)
const recycleMaxPath = 259;

View File

@ -301,7 +301,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
}
for (var i = 0; i < 2; i++) {
final pane = _panes[i];
unawaited(_goTo(pane, wants[i], remember: false, focus: false).then((_) => _reveal(pane, pane.current)));
unawaited(_goTo(pane, wants[i], remember: false).then((_) => _reveal(pane, pane.current)));
}
}
@ -594,7 +594,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
/// 그 폴더로: 저장 장치부터 그 폴더까지 펼치고 지금 폴더로 정한다
/// [fresh]: 경로 입력 · 찾기 · 내역으로 갈 때 - 그 폴더를 새로 읽는다 (앱 밖에서 바뀐 것까지 보이게, 122)
Future<void> _goTo(_Pane pane, String dir, {bool remember = true, bool fresh = false, bool focus = true}) async {
/// 폴더로 간다. 그 폴더를 "고른 항목" 으로 잡지 않는다 (48 보충 · 관리자: 누르거나 표시하지 않은 폴더가 복사 · 이동 대상이 되지 않게).
/// 위치는 지금 폴더 표시 · 스크롤로 보인다. [focus] 는 그 폴더를 일부러 고를 때만
Future<void> _goTo(_Pane pane, String dir, {bool remember = true, bool fresh = false, bool focus = false}) async {
// 124 · 128: 사용자가 WebDAV 로 간 것이면 마스터 창을 한 번 취소했어도 다시 묻는다 (켤 때 되살리는 것은 [remember] 없음)
if (remember && isDav(dir)) await SecretGate.pass(force: true);
if (!mounted) return;
@ -1331,12 +1333,16 @@ class _ExplorerPageState extends State<ExplorerPage> {
// 하나씩: 실패해도 나머지는 계속. 실제로 어떻게 됐는지 세어 그대로 알린다 (94)
final failed = <(String, String)>[];
var recycled = 0, nuked = 0, kept = 0;
// 148: 휴지통으로 보낸 것 (되돌리기용) - 정보 파일 시각은 초 단위라 조금 앞에서부터
final since = DateTime.now().subtract(const Duration(seconds: 2));
final sent = <String>[];
for (final x in paths) {
try {
if (!permanent) {
switch (moveToRecycleBin(x)) {
case RecycleResult.recycled:
recycled++;
sent.add(x);
case RecycleResult.deletedPermanently:
nuked++;
case RecycleResult.cancelled:
@ -1370,13 +1376,19 @@ class _ExplorerPageState extends State<ExplorerPage> {
actions: [FilledButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('확인')))],
),
);
} else if (!permanent) {
_snack([
} else if (!permanent && mounted) {
final text = [
if (recycled > 0) trf('{0}개 항목을 휴지통으로 보냈습니다.', [recycled]),
// 휴지통보다 커서 Windows 가 물었고 사용자가 영구 삭제를 골랐다
if (nuked > 0) trf('{0}개는 휴지통에 들어가지 않아 영구 삭제했습니다 (Windows 가 묻고 고른 대로).', [nuked]),
if (kept > 0) trf('{0}개는 지우지 않았습니다 (취소).', [kept]),
].join(' '));
].join(' ');
// 148: 휴지통으로 보낸 것은 바로 [되돌리기]
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
duration: const Duration(seconds: 10),
content: Text(text),
action: sent.isEmpty ? null : SnackBarAction(label: tr('되돌리기'), onPressed: () => _undoRecycle(sent, since)),
));
}
for (final x in _panes) {
x.marked.removeAll(paths);
@ -1387,6 +1399,28 @@ class _ExplorerPageState extends State<ExplorerPage> {
await _refreshAll(paths.map(p.dirname));
}
/// 148: 방금 휴지통으로 보낸 것을 원래 자리로 (원래 자리에 같은 이름이 생겼으면 그것은 두고 알림)
Future<void> _undoRecycle(List<String> paths, DateTime since) async {
final failed = <String>[];
var back = 0;
for (final x in paths) {
try {
restoreFromRecycleBin(x, since: since);
back++;
} catch (e) {
failed.add('${vBasename(x)}: ${e is FileSystemException ? e.message : e}');
}
}
await _refreshAll(paths.map(p.dirname));
for (final x in _panes) {
x.changed();
}
_snack([
if (back > 0) trf('{0}개 항목을 되돌렸습니다.', [back]),
if (failed.isNotEmpty) trf('되돌리지 못함: {0}', [failed.take(3).join(' · ')]),
].join(' '));
}
/// 다른 창의 지금 폴더로 복사 · 이동 (진행 창 · 취소)
/// 복사 · 이동 버튼: 두 창이면 다른 창의 지금 폴더로, 그 밖의 배치는 담아 두기 (붙여넣기로)
Future<void> _transfer(_Pane pane, List<String> sources, {required bool move}) async {

View File

@ -103,6 +103,15 @@ void main() {
expect(find.widgetWithText(FilledButton, '복사'), findsNothing, reason: '확인 창이 뜨지 않음');
expect(find.textContaining('복사할 항목을 고르세요'), findsOneWidget);
expect(Directory(p.join(right, 'left')).existsSync(), isFalse);
// 상위 폴더로 가도 (사용자 이동) 그 폴더를 대상으로 잡지 않는다
await tester.runAsync(() => tester.tap(find.text('상위 폴더').first));
await settle(tester, () => false, rounds: 15);
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
await tester.pump();
await tester.runAsync(() => tester.tap(find.text('복사').first));
await tester.pump();
expect(find.widgetWithText(FilledButton, '복사'), findsNothing);
expect(find.textContaining('복사할 항목을 고르세요'), findsOneWidget);
});
testWidgets('52: 복사 중에 화면을 떠났다 돌아와도 진행 막대가 다시 보이고, 끝나면 받는 폴더를 새로 읽는다 (.jjpart 가 남아 보이지 않음)', (tester) async {
@ -132,6 +141,31 @@ void main() {
expect(find.textContaining('.jjpart'), findsNothing);
});
testWidgets('148: 휴지통으로 보낸 뒤 알림의 [되돌리기] 를 누르면 원래 자리로', (tester) async {
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('doc.txt').evaluate().length >= 2);
await tester.runAsync(() => tester.tap(find.text('doc.txt').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.text('되돌리기').evaluate().isNotEmpty);
expect(File(p.join(left, 'doc.txt')).existsSync(), isFalse);
expect(find.textContaining('1개 항목을 휴지통으로 보냈습니다.'), findsOneWidget);
// 알림의 버튼은 시험의 가짜 시간 안에서 누른다 (알림 닫기 애니메이션이 시험 밖에서 돌지 않게)
await tester.tap(find.text('되돌리기'));
await tester.pump();
await settle(tester, () => find.textContaining('되돌렸습니다').evaluate().isNotEmpty);
expect(File(p.join(left, 'doc.txt')).readAsStringSync(), 'new');
expect(find.textContaining('1개 항목을 되돌렸습니다.'), findsOneWidget);
// 알림 (10초) 이 시험이 끝난 뒤에 닫히지 않게 정리
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
await tester.pumpAndSettle();
}, skip: !Platform.isWindows);
testWidgets('48: 건너뛰기를 고르면 그대로 두고 건너뛴 것을 알린다', (tester) async {
await startCopy(tester);
await tester.tap(find.text('건너뛰기'));

View File

@ -53,6 +53,54 @@ void main() {
}
}, skip: !Platform.isWindows);
test('148: 휴지통으로 보낸 파일 · 폴더를 원래 자리로 되돌린다 (휴지통에 남지 않음) · 원래 자리에 같은 이름이 있으면 덮지 않음', () async {
final tmp = Directory.systemTemp.createTempSync('jj_undo_');
addTearDown(() async {
await _purgeFromRecycleBin(tmp.absolute.path); // 실패해 남은 것이 있으면 사용자 휴지통에서 지운다
if (tmp.existsSync()) tmp.deleteSync(recursive: true);
});
final since = DateTime.now().subtract(const Duration(seconds: 2));
final f = File(p.join(tmp.absolute.path, 'undo.txt'))..writeAsStringSync('keep me');
final d = Directory(p.join(tmp.absolute.path, 'undo_dir'))..createSync();
File(p.join(d.path, 'a.txt')).writeAsStringSync('a');
expect(moveToRecycleBin(f.path), RecycleResult.recycled);
expect(moveToRecycleBin(d.path), RecycleResult.recycled);
expect(f.existsSync() || d.existsSync(), isFalse);
restoreFromRecycleBin(f.path, since: since);
restoreFromRecycleBin(d.path, since: since);
expect(f.readAsStringSync(), 'keep me');
expect(File(p.join(d.path, 'a.txt')).readAsStringSync(), 'a');
expect(recycledInfoExists(f.path, since: since), isFalse, reason: '휴지통에 남지 않음');
// 원래 자리에 같은 이름이 생겼으면 덮지 않고 실패 (휴지통의 것은 그대로)
expect(moveToRecycleBin(f.path), RecycleResult.recycled);
f.writeAsStringSync('new one');
expect(() => restoreFromRecycleBin(f.path, since: since), throwsA(isA<FileSystemException>()));
expect(f.readAsStringSync(), 'new one');
expect(recycledInfoExists(f.path, since: since), isTrue);
// 휴지통에 없으면 실패
expect(() => restoreFromRecycleBin(p.join(tmp.absolute.path, 'never.txt'), since: since), throwsA(isA<FileSystemException>()));
}, skip: !Platform.isWindows);
test('148: 원래 폴더가 그사이 지워졌으면 다시 만들지 않고 알린다 · 이 사용자 (SID) 의 휴지통에서만 찾는다', () async {
expect(currentUserSid(), matches(RegExp(r'^S-1-5-')));
final tmp = Directory.systemTemp.createTempSync('jj_undo_');
addTearDown(() async {
await _purgeFromRecycleBin(tmp.absolute.path);
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(); // 사용자가 그 폴더를 지움
expect(
() => restoreFromRecycleBin(f.path, since: since),
throwsA(isA<FileSystemException>()
.having((e) => e.message, 'message', contains('원래 폴더가 없어 되돌리지 못했습니다'))));
expect(folder.existsSync(), isFalse, reason: '지운 폴더를 몰래 다시 만들지 않음');
expect(recycledInfoExists(f.path, since: since), isTrue, reason: '휴지통의 것은 그대로');
}, skip: !Platform.isWindows);
test('긴 경로 (260자 넘음) 는 휴지통으로 보내지 않고 이유를 알린다 - 묻지 않고 영구 삭제되던 것 (폴더 안에 있어도)', () {
if (!Platform.isWindows) return;
// 긴 경로를 만들고 지우려면 앞에 \\?\ 를 붙인다