휴지통 시험: 기본은 가짜 휴지통, 실제 휴지통은 JJ_TEST_REAL_RECYCLE=1 일 때만

전체 시험 도중에 끊기면 사용자 휴지통에 시험 흔적이 남았다 (10/10 01:31).
- 탐색기는 recycle() · restoreRecycled() 를 쓴다 (앱은 늘 실제 휴지통, 시험만 recycleBinForTest 로 바꿈).
- test/support/fake_recycle_bin.dart: 실제와 같은 규칙 (긴 경로 거부 · 되돌릴 때 덮지 않음 · 원래 폴더 없으면 안 만듦),
  다른 드라이브면 복사 후 지움. useTestRecycleBin() 은 JJ_TEST_REAL_RECYCLE=1 이 아니면 가짜로.
- recycle_bin_test 의 실제 휴지통 시험 3개와 긴 경로의 실제 함수 부르기는 실제 모드에서만.
  explorer_conflict · explorer · 캡처 시험 (b3 · check) 은 기본 가짜.
- windows_check_shot_test 의 "65 · 94 · 98" 은 144 뒤로 깨져 있었다 ([그대로 두기] 창을 닫지 않아 다음 크기에서 막힘) - 닫게 고침.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음4
RyuJongJin 2026-10-10 02:09:36 +09:00
parent 5601f9d4fb
commit f33a4b3547
8 changed files with 160 additions and 11 deletions

View File

@ -12,6 +12,7 @@ import 'package:jj_mkvmaker/ui/explorer_page.dart';
import 'package:jj_mkvmaker/ui/theme.dart'; import 'package:jj_mkvmaker/ui/theme.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import '../test/support/fake_recycle_bin.dart';
import '../test/support/recycle_leftovers.dart'; import '../test/support/recycle_leftovers.dart';
/// 묶음 3 (데이터 쪽) 기기 감독 확인용 (Windows): 실제 앱 화면을 띄워 누르고 단계마다 PNG (JJ_SHOT_DIR) 로 남긴다. /// 묶음 3 (데이터 쪽) 기기 감독 확인용 (Windows): 실제 앱 화면을 띄워 누르고 단계마다 PNG (JJ_SHOT_DIR) 로 남긴다.
@ -77,6 +78,8 @@ void main() {
..writeAsStringSync('old'); ..writeAsStringSync('old');
c = AppController(PlatformServices.create()); c = AppController(PlatformServices.create());
c.settings.explorerPaths = [left, right]; c.settings.explorerPaths = [left, right];
// 휴지통: 기본은 가짜 (화면 흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통
useTestRecycleBin();
}); });
tearDown(() async { tearDown(() async {
ExplorerPage.debugVolumes = null; ExplorerPage.debugVolumes = null;

View File

@ -19,6 +19,8 @@ import 'package:jj_mkvmaker/ui/security_settings.dart';
import 'package:jj_mkvmaker/ui/theme.dart'; import 'package:jj_mkvmaker/ui/theme.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import '../test/support/fake_recycle_bin.dart';
/// 기기 감독 (Windows) 확인용: 실제 앱 화면을 띄워 누르고 단계마다 PNG (JJ_SHOT_DIR) 로 남긴다. /// 기기 감독 (Windows) 확인용: 실제 앱 화면을 띄워 누르고 단계마다 PNG (JJ_SHOT_DIR) 로 남긴다.
/// 65 · 94 · 98 (휴지통 · 휴지통 없는 위치 · 오류 문구), 37 (경로 입력), 124 · 130 (마스터 · 최상), 123 (AI 그림). /// 65 · 94 · 98 (휴지통 · 휴지통 없는 위치 · 오류 문구), 37 (경로 입력), 124 · 130 (마스터 · 최상), 123 (AI 그림).
/// 시험 폴더 · 메모리 저장소만 쓴다 (사용자 설정 · 비밀번호는 건드리지 않음). 휴지통에 넣은 시험 항목은 끝에 $I 로 골라 지운다. /// 시험 폴더 · 메모리 저장소만 쓴다 (사용자 설정 · 비밀번호는 건드리지 않음). 휴지통에 넣은 시험 항목은 끝에 $I 로 골라 지운다.
@ -78,6 +80,8 @@ void main() {
testWidgets('65 · 94 · 98: 지우기 - 휴지통 · 영구 삭제 · 휴지통 없는 위치 · 오류 문구', (t) async { testWidgets('65 · 94 · 98: 지우기 - 휴지통 · 영구 삭제 · 휴지통 없는 위치 · 오류 문구', (t) async {
final tmp = tempDir('jj_shot_del_'); final tmp = tempDir('jj_shot_del_');
final started = DateTime.now(); final started = DateTime.now();
// 휴지통: 기본은 가짜 (화면 흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통 (끝에 시험 항목만 지움)
useTestRecycleBin();
try { try {
for (final (tag, size) in sizes) { for (final (tag, size) in sizes) {
final dir = Directory(p.join(tmp.path, tag))..createSync(); final dir = Directory(p.join(tmp.path, tag))..createSync();
@ -151,6 +155,13 @@ void main() {
await t.tap(find.text('휴지통으로').last); await t.tap(find.text('휴지통으로').last);
await settle(t, 20); await settle(t, 20);
await shot(t, '98_error_text_$tag'); await shot(t, '98_error_text_$tag');
// 144 뒤로는 이유 한 줄과 [그대로 두기] / [영구 삭제] 창이 뜬다 - 지우지 않고 닫아 다음 크기로
for (final b in ['그대로 두기', '확인']) {
if (find.text(b).evaluate().isNotEmpty) {
await t.tap(find.text(b).last);
await settle(t);
}
}
} }
} finally { } finally {
await t.pumpWidget(const SizedBox()); await t.pumpWidget(const SizedBox());

View File

@ -2,6 +2,7 @@ import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart' show visibleForTesting;
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import '../../l10n/tr.dart'; import '../../l10n/tr.dart';
@ -18,6 +19,26 @@ enum RecycleResult {
cancelled, cancelled,
} }
/// 시험용: 실제 휴지통 대신 쓸 것. 기본 전체 시험은 가짜로 흐름만 본다 - 시험이 도중에 끊기면 사용자 휴지통에
/// 흔적이 남기 때문 (실제 휴지통 시험은 JJ_TEST_REAL_RECYCLE=1 일 때만, test/support/fake_recycle_bin.dart).
abstract class RecycleBinBackend {
RecycleResult move(String path);
void restore(String original, {required DateTime since});
}
/// null 이면 실제 휴지통 (앱은 늘 null)
@visibleForTesting
RecycleBinBackend? recycleBinForTest;
/// 휴지통으로 보내기 (앱의 화면은 이것을 쓴다 - 시험에서 가짜로 바꿀 수 있게)
RecycleResult recycle(String path) => recycleBinForTest?.move(path) ?? moveToRecycleBin(path);
/// 휴지통에서 원래 자리로 (앱의 화면은 이것을 쓴다)
void restoreRecycled(String original, {required DateTime since}) {
final t = recycleBinForTest;
t == null ? restoreFromRecycleBin(original, since: since) : t.restore(original, since: since);
}
/// 이 경로가 있는 곳에 휴지통이 있는지 (94). Windows 의 휴지통은 고정 디스크에만 있다: /// 이 경로가 있는 곳에 휴지통이 있는지 (94). Windows 의 휴지통은 고정 디스크에만 있다:
/// 네트워크 드라이브 (Z:) · \\NAS\공유 · USB 메모리 · CD 는 휴지통 없이 바로 지워진다. /// 네트워크 드라이브 (Z:) · \\NAS\공유 · USB 메모리 · CD 는 휴지통 없이 바로 지워진다.
bool hasRecycleBin(String path) { bool hasRecycleBin(String path) {

View File

@ -1428,7 +1428,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
for (final x in paths) { for (final x in paths) {
try { try {
if (!permanent) { if (!permanent) {
switch (moveToRecycleBin(x)) { switch (recycle(x)) {
case RecycleResult.recycled: case RecycleResult.recycled:
recycled++; recycled++;
sent.add(x); sent.add(x);
@ -1530,7 +1530,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
var back = 0; var back = 0;
for (final x in paths) { for (final x in paths) {
try { try {
restoreFromRecycleBin(x, since: since); restoreRecycled(x, since: since);
back++; back++;
} catch (e) { } catch (e) {
failed.add('${vBasename(x)}: ${e is FileSystemException ? e.message : e}'); failed.add('${vBasename(x)}: ${e is FileSystemException ? e.message : e}');

View File

@ -10,6 +10,7 @@ import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/ui/explorer_page.dart'; import 'package:jj_mkvmaker/ui/explorer_page.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'support/fake_recycle_bin.dart';
import 'support/recycle_leftovers.dart'; import 'support/recycle_leftovers.dart';
/// 48 · 154 · 155: 같은 이름이 있으면 덮어쓰기 · 건너뛰기 · 이름 바꾸기를 고르고, 확인 창에 항목 이름이 보인다 /// 48 · 154 · 155: 같은 이름이 있으면 덮어쓰기 · 건너뛰기 · 이름 바꾸기를 고르고, 확인 창에 항목 이름이 보인다
@ -17,6 +18,7 @@ void main() {
late Directory tmp; late Directory tmp;
late String left, right; late String left, right;
late AppController c; late AppController c;
FakeRecycleBin? bin;
setUp(() { setUp(() {
tmp = Directory.systemTemp.createTempSync('jj_conflict_'); tmp = Directory.systemTemp.createTempSync('jj_conflict_');
@ -32,10 +34,12 @@ void main() {
c.settings.explorerPaths = [left, right]; c.settings.explorerPaths = [left, right];
final root = p.rootPrefix(tmp.path); final root = p.rootPrefix(tmp.path);
ExplorerPage.debugVolumes = () => [(root, root.replaceAll(RegExp(r'[\\/]+$'), ''))]; ExplorerPage.debugVolumes = () => [(root, root.replaceAll(RegExp(r'[\\/]+$'), ''))];
// 휴지통: 기본은 가짜 (흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통
bin = useTestRecycleBin();
}); });
tearDown(() async { tearDown(() async {
ExplorerPage.debugVolumes = null; ExplorerPage.debugVolumes = null;
// 휴지통을 건드리는 시험 (148 · 144): 사용자 휴지통에 이 시험 폴더 아래의 흔적이 없는지 스스로 확인 // 사용자 휴지통에 이 시험 폴더 아래의 흔적이 없는지 스스로 확인 (가짜일 때도 - 실제 휴지통에 닿지 않았는지)
expect(await recycleLeftovers(tmp.path), 0, reason: '사용자 휴지통에 시험 흔적 없음 (하위 폴더 포함)'); expect(await recycleLeftovers(tmp.path), 0, reason: '사용자 휴지통에 시험 흔적 없음 (하위 폴더 포함)');
tmp.deleteSync(recursive: true); tmp.deleteSync(recursive: true);
}); });
@ -166,6 +170,7 @@ void main() {
await settle(tester, () => find.textContaining('되돌렸습니다').evaluate().isNotEmpty); await settle(tester, () => find.textContaining('되돌렸습니다').evaluate().isNotEmpty);
expect(File(p.join(left, 'doc.txt')).readAsStringSync(), 'new'); expect(File(p.join(left, 'doc.txt')).readAsStringSync(), 'new');
expect(find.textContaining('1개 항목을 되돌렸습니다.'), findsOneWidget); expect(find.textContaining('1개 항목을 되돌렸습니다.'), findsOneWidget);
if (bin != null) expect(bin!.items, isEmpty, reason: '가짜 휴지통에서 되돌림');
// 알림 (10초) 이 시험이 끝난 뒤에 닫히지 않게 정리 // 알림 (10초) 이 시험이 끝난 뒤에 닫히지 않게 정리
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars(); ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
await tester.pumpAndSettle(); await tester.pumpAndSettle();

View File

@ -11,6 +11,7 @@ import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/ui/explorer_page.dart'; import 'package:jj_mkvmaker/ui/explorer_page.dart';
import 'package:jj_mkvmaker/ui/path_label.dart'; import 'package:jj_mkvmaker/ui/path_label.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'support/fake_recycle_bin.dart';
/// 다른 앱으로 열기를 기록만 하는 셸 (실제로 프로그램을 띄우지 않게) /// 다른 앱으로 열기를 기록만 하는 셸 (실제로 프로그램을 띄우지 않게)
class _Shell extends NoopShell { class _Shell extends NoopShell {
@ -29,6 +30,8 @@ void main() {
late AppController c; late AppController c;
setUp(() { setUp(() {
// 휴지통: 기본은 가짜 (이 시험들은 영구 삭제를 고르지만 혹시라도 사용자 휴지통에 닿지 않게)
useTestRecycleBin();
tmp = Directory.systemTemp.createTempSync('jj_explorer_'); tmp = Directory.systemTemp.createTempSync('jj_explorer_');
left = p.join(tmp.path, 'left'); left = p.join(tmp.path, 'left');
right = p.join(tmp.path, 'right'); right = p.join(tmp.path, 'right');

View File

@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart'; import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'support/fake_recycle_bin.dart';
import 'support/recycle_leftovers.dart'; import 'support/recycle_leftovers.dart';
/// 시험이 휴지통에 넣은 것만 (원래 위치가 이 시험의 임시 폴더인 항목) 휴지통에서 지운다. 사용자 항목은 건드리지 않는다. /// 시험이 휴지통에 넣은 것만 (원래 위치가 이 시험의 임시 폴더인 항목) 휴지통에서 지운다. 사용자 항목은 건드리지 않는다.
@ -30,7 +31,9 @@ foreach (\$it in @(\$rb.Items())) {
return int.tryParse('${r.stdout}'.trim().split('\n').last.trim()) ?? -1; return int.tryParse('${r.stdout}'.trim().split('\n').last.trim()) ?? -1;
} }
/// 65 · 94 · 98: Windows 휴지통 (실제 휴지통 - 시험 파일 하나 · 폴더 하나, 끝나면 휴지통에서도 지움) /// 65 · 94 · 98: Windows 휴지통 (실제 휴지통 - 시험 파일 하나 · 폴더 하나, 끝나면 휴지통에서도 지움).
/// 실제 휴지통을 쓰는 시험은 JJ_TEST_REAL_RECYCLE=1 일 때만 (전체 시험 중에 끊기면 사용자 휴지통에 흔적이 남으므로).
/// 휴지통 쪽 코드를 고칠 때 · 릴리스 전에 따로: JJ_TEST_REAL_RECYCLE=1 flutter test test/recycle_bin_test.dart
void main() { void main() {
test('파일 · 폴더를 휴지통으로 (실제로 들어갔는지까지) · 없는 것은 그냥 성공', () async { test('파일 · 폴더를 휴지통으로 (실제로 들어갔는지까지) · 없는 것은 그냥 성공', () async {
final tmp = Directory.systemTemp.createTempSync('jj_recycle_'); final tmp = Directory.systemTemp.createTempSync('jj_recycle_');
@ -54,7 +57,7 @@ void main() {
// 사용자 휴지통에 시험 항목을 남기지 않는다 // 사용자 휴지통에 시험 항목을 남기지 않는다
expect(await _purgeFromRecycleBin(tmp.absolute.path), 2); expect(await _purgeFromRecycleBin(tmp.absolute.path), 2);
} }
}, skip: !Platform.isWindows); }, skip: realRecycleSkip);
test('148: 휴지통으로 보낸 파일 · 폴더를 원래 자리로 되돌린다 (휴지통에 남지 않음) · 원래 자리에 같은 이름이 있으면 덮지 않음', () async { test('148: 휴지통으로 보낸 파일 · 폴더를 원래 자리로 되돌린다 (휴지통에 남지 않음) · 원래 자리에 같은 이름이 있으면 덮지 않음', () async {
final tmp = Directory.systemTemp.createTempSync('jj_undo_'); final tmp = Directory.systemTemp.createTempSync('jj_undo_');
@ -83,7 +86,7 @@ void main() {
expect(recycledInfoExists(f.path, since: since), isTrue); expect(recycledInfoExists(f.path, since: since), isTrue);
// 휴지통에 없으면 실패 // 휴지통에 없으면 실패
expect(() => restoreFromRecycleBin(p.join(tmp.absolute.path, 'never.txt'), since: since), throwsA(isA<FileSystemException>())); expect(() => restoreFromRecycleBin(p.join(tmp.absolute.path, 'never.txt'), since: since), throwsA(isA<FileSystemException>()));
}, skip: !Platform.isWindows); }, skip: realRecycleSkip);
test('148: 원래 폴더가 그사이 지워졌으면 다시 만들지 않고 알린다 · 이 사용자 (SID) 의 휴지통에서만 찾는다', () async { test('148: 원래 폴더가 그사이 지워졌으면 다시 만들지 않고 알린다 · 이 사용자 (SID) 의 휴지통에서만 찾는다', () async {
expect(currentUserSid(), matches(RegExp(r'^S-1-5-'))); expect(currentUserSid(), matches(RegExp(r'^S-1-5-')));
@ -105,7 +108,7 @@ void main() {
.having((e) => e.message, 'message', contains('원래 폴더가 없어 되돌리지 못했습니다')))); .having((e) => e.message, 'message', contains('원래 폴더가 없어 되돌리지 못했습니다'))));
expect(folder.existsSync(), isFalse, reason: '지운 폴더를 몰래 다시 만들지 않음'); expect(folder.existsSync(), isFalse, reason: '지운 폴더를 몰래 다시 만들지 않음');
expect(recycledInfoExists(f.path, since: since), isTrue, reason: '휴지통의 것은 그대로'); expect(recycledInfoExists(f.path, since: since), isTrue, reason: '휴지통의 것은 그대로');
}, skip: !Platform.isWindows); }, skip: realRecycleSkip);
test('긴 경로 (260자 넘음) 는 휴지통으로 보내지 않고 이유를 알린다 - 묻지 않고 영구 삭제되던 것 (폴더 안에 있어도)', () { test('긴 경로 (260자 넘음) 는 휴지통으로 보내지 않고 이유를 알린다 - 묻지 않고 영구 삭제되던 것 (폴더 안에 있어도)', () {
if (!Platform.isWindows) return; if (!Platform.isWindows) return;
@ -122,13 +125,18 @@ void main() {
File(lp(file)).writeAsStringSync('x'); File(lp(file)).writeAsStringSync('x');
expect(file.length, greaterThan(259)); expect(file.length, greaterThan(259));
expect(longPathInside(file), file); expect(longPathInside(file), file);
expect(() => moveToRecycleBin(file), throwsA(isA<FileSystemException>().having((e) => e.message, 'message', contains('경로가 너무 깁니다')))); // 실제 휴지통 함수는 JJ_TEST_REAL_RECYCLE=1 일 때만 (고장 나면 사용자 휴지통에 들어가므로)
expect(File(lp(file)).existsSync(), isTrue, reason: '지우지 않음'); if (realRecycleBin) {
expect(() => moveToRecycleBin(file), throwsA(isA<FileSystemException>().having((e) => e.message, 'message', contains('경로가 너무 깁니다'))));
expect(File(lp(file)).existsSync(), isTrue, reason: '지우지 않음');
}
// 폴더 자체는 짧아도 안에 긴 경로가 있으면 // 폴더 자체는 짧아도 안에 긴 경로가 있으면
final outer = p.join(tmp.path, 'outer'); final outer = p.join(tmp.path, 'outer');
expect(longPathInside(outer), isNotNull); expect(longPathInside(outer), isNotNull);
expect(() => moveToRecycleBin(outer), throwsA(isA<FileSystemException>())); if (realRecycleBin) {
expect(Directory(outer).existsSync(), isTrue); expect(() => moveToRecycleBin(outer), throwsA(isA<FileSystemException>()));
expect(Directory(outer).existsSync(), isTrue);
}
final short = File(p.join(tmp.path, 'short.txt'))..writeAsStringSync('x'); final short = File(p.join(tmp.path, 'short.txt'))..writeAsStringSync('x');
expect(longPathInside(short.path), isNull, reason: '짧은 파일 경로'); expect(longPathInside(short.path), isNull, reason: '짧은 파일 경로');
}); });

View File

@ -0,0 +1,98 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/l10n/tr.dart';
import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart';
import 'package:path/path.dart' as p;
/// 실제 사용자 휴지통을 쓰는 시험은 이 값이 1 일 때만 (휴지통 쪽 코드를 고칠 때 · 릴리스 전에 따로 돌리고 흔적 0 확인).
/// 기본 전체 시험은 [FakeRecycleBin] 으로 흐름만 본다 - 시험이 도중에 끊기면 사용자 휴지통에 흔적이 남기 때문.
bool get realRecycleBin => Platform.environment['JJ_TEST_REAL_RECYCLE'] == '1';
/// 실제 휴지통 시험을 건너뛸 때의 이유 (skip 에 그대로)
Object get realRecycleSkip => !Platform.isWindows
? 'Windows 만'
: realRecycleBin
? false
: '실제 휴지통 시험은 JJ_TEST_REAL_RECYCLE=1 일 때만';
/// 시험 하나 동안 휴지통을 가짜로 (JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통 그대로). 가짜 휴지통을 돌려준다 (실제면 null).
FakeRecycleBin? useTestRecycleBin() {
if (realRecycleBin) return null;
final fake = FakeRecycleBin();
recycleBinForTest = fake;
addTearDown(() {
recycleBinForTest = null;
fake.dispose();
});
return fake;
}
/// 시험용 휴지통: 시험 임시 폴더 안의 자기 폴더로 옮기고, 실제 휴지통과 같은 규칙으로 되돌린다
/// (긴 경로는 받지 않음 · 원래 자리에 같은 이름이 있으면 덮지 않음 · 원래 폴더가 없으면 다시 만들지 않음).
class FakeRecycleBin implements RecycleBinBackend {
final Directory bin = Directory.systemTemp.createTempSync('jj_fake_recycle_');
/// 원래 경로 → 휴지통 안의 경로 (보낸 차례대로)
final items = <String, String>{};
var _n = 0;
@override
RecycleResult move(String path) {
if (FileSystemEntity.typeSync(path, followLinks: false) == FileSystemEntityType.notFound) return RecycleResult.recycled;
final long = longPathInside(path);
if (long != null) throw LongPathException(recycleErrorText(0x7C), long);
final full = File(path).absolute.path;
final to = p.join(bin.path, '\$R${_n++}${p.extension(full)}');
_move(full, to);
items[full] = to;
return RecycleResult.recycled;
}
@override
void restore(String original, {required DateTime since}) {
final from = items[File(original).absolute.path];
if (from == 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);
}
_move(from, original);
items.remove(File(original).absolute.path);
}
/// 옮기기 (다른 드라이브면 복사한 뒤 지운다 - 시험 폴더가 M: 이고 가짜 휴지통이 C: 의 임시 폴더일 때)
static void _move(String from, String to) {
final dir = FileSystemEntity.isDirectorySync(from);
try {
dir ? Directory(from).renameSync(to) : File(from).renameSync(to);
return;
} on FileSystemException {
// 아래에서 복사
}
if (!dir) {
File(from).copySync(to);
File(from).deleteSync();
return;
}
Directory(to).createSync(recursive: true);
for (final e in Directory(from).listSync(recursive: true, followLinks: false)) {
final dst = p.join(to, p.relative(e.path, from: from));
if (e is Directory) {
Directory(dst).createSync(recursive: true);
} else if (e is File) {
File(dst).parent.createSync(recursive: true);
e.copySync(dst);
}
}
Directory(from).deleteSync(recursive: true);
}
void dispose() {
try {
bin.deleteSync(recursive: true);
} catch (_) {}
}
}