166 lines
9.5 KiB
Dart
166 lines
9.5 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart';
|
|
import 'package:path/path.dart' as p;
|
|
|
|
import 'support/fake_recycle_bin.dart';
|
|
import 'support/recycle_leftovers.dart';
|
|
|
|
/// 시험이 휴지통에 넣은 것만 (원래 위치가 이 시험의 임시 폴더인 항목) 휴지통에서 지운다. 사용자 항목은 건드리지 않는다.
|
|
Future<int> _purgeFromRecycleBin(String fromDir) async {
|
|
final dir = fromDir.replaceAll("'", "''");
|
|
final r = await Process.run('powershell.exe', [
|
|
'-NoProfile',
|
|
'-NonInteractive',
|
|
'-Command',
|
|
'''
|
|
\$rb = (New-Object -ComObject Shell.Application).Namespace(10)
|
|
\$n = 0
|
|
foreach (\$it in @(\$rb.Items())) {
|
|
if (\$rb.GetDetailsOf(\$it, 1) -ne '$dir') { continue }
|
|
\$path = \$it.Path; \$d = Split-Path \$path; \$leaf = Split-Path \$path -Leaf
|
|
Remove-Item -LiteralPath \$path -Recurse -Force -Confirm:\$false
|
|
\$info = Join-Path \$d ('\$I' + \$leaf.Substring(2))
|
|
if (Test-Path -LiteralPath \$info) { Remove-Item -LiteralPath \$info -Force -Confirm:\$false }
|
|
\$n++
|
|
}
|
|
\$n
|
|
''',
|
|
]);
|
|
return int.tryParse('${r.stdout}'.trim().split('\n').last.trim()) ?? -1;
|
|
}
|
|
|
|
/// 65 · 94 · 98: Windows 휴지통 (실제 휴지통 - 시험 파일 하나 · 폴더 하나, 끝나면 휴지통에서도 지움).
|
|
/// 실제 휴지통을 쓰는 시험은 JJ_TEST_REAL_RECYCLE=1 일 때만 (전체 시험 중에 끊기면 사용자 휴지통에 흔적이 남으므로).
|
|
/// 휴지통 쪽 코드를 고칠 때 · 릴리스 전에 따로: JJ_TEST_REAL_RECYCLE=1 flutter test test/recycle_bin_test.dart
|
|
void main() {
|
|
test('파일 · 폴더를 휴지통으로 (실제로 들어갔는지까지) · 없는 것은 그냥 성공', () async {
|
|
final tmp = Directory.systemTemp.createTempSync('jj_recycle_');
|
|
addTearDown(() async {
|
|
expect(await recycleLeftovers(tmp.path), 0, reason: '사용자 휴지통에 시험 흔적 없음 (하위 폴더 포함)');
|
|
if (tmp.existsSync()) tmp.deleteSync(recursive: true);
|
|
});
|
|
final f = File(p.join(tmp.path, 'jj_recycle_test.txt'))..writeAsStringSync('x');
|
|
final d = Directory(p.join(tmp.path, 'jj_recycle_dir'))..createSync();
|
|
File(p.join(d.path, 'a.txt')).writeAsStringSync('a');
|
|
try {
|
|
expect(hasRecycleBin(f.path), isTrue); // 임시 폴더는 고정 디스크
|
|
expect(moveToRecycleBin(f.path), RecycleResult.recycled);
|
|
// 수를 세지 않고 휴지통 안의 정보 파일 ($I, 원래 경로) 로 확인할 수 있다
|
|
expect(recycledInfoExists(f.absolute.path, since: DateTime.now().subtract(const Duration(minutes: 1))), isTrue);
|
|
expect(f.existsSync(), isFalse);
|
|
expect(moveToRecycleBin(d.path), RecycleResult.recycled);
|
|
expect(d.existsSync(), isFalse);
|
|
expect(moveToRecycleBin(p.join(tmp.path, 'none.txt')), RecycleResult.recycled);
|
|
} finally {
|
|
// 사용자 휴지통에 시험 항목을 남기지 않는다
|
|
expect(await _purgeFromRecycleBin(tmp.absolute.path), 2);
|
|
}
|
|
}, skip: realRecycleSkip);
|
|
|
|
test('148: 휴지통으로 보낸 파일 · 폴더를 원래 자리로 되돌린다 (휴지통에 남지 않음) · 원래 자리에 같은 이름이 있으면 덮지 않음', () async {
|
|
final tmp = Directory.systemTemp.createTempSync('jj_undo_');
|
|
addTearDown(() async {
|
|
await _purgeFromRecycleBin(tmp.absolute.path); // 실패해 남은 것이 있으면 사용자 휴지통에서 지운다
|
|
expect(await recycleLeftovers(tmp.path), 0, reason: '사용자 휴지통에 시험 흔적 없음 (하위 폴더 포함)');
|
|
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: realRecycleSkip);
|
|
|
|
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 {
|
|
// 휴지통에 남긴 x.txt 의 원래 위치는 gone 폴더 (정확히 그 폴더로 찾아 지운다)
|
|
expect(await _purgeFromRecycleBin(folder.path), 1);
|
|
expect(await recycleLeftovers(tmp.path), 0, reason: '사용자 휴지통에 시험 흔적 없음 (하위 폴더 포함)');
|
|
if (tmp.existsSync()) tmp.deleteSync(recursive: true);
|
|
});
|
|
final since = DateTime.now().subtract(const Duration(seconds: 2));
|
|
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: realRecycleSkip);
|
|
|
|
test('긴 경로 (260자 넘음) 는 휴지통으로 보내지 않고 이유를 알린다 - 묻지 않고 영구 삭제되던 것 (폴더 안에 있어도)', () {
|
|
if (!Platform.isWindows) return;
|
|
// 긴 경로를 만들고 지우려면 앞에 \\?\ 를 붙인다
|
|
String lp(String s) => r'\\?\' + s;
|
|
final tmp = Directory.systemTemp.createTempSync('jj_rb_long_');
|
|
addTearDown(() => Directory(lp(tmp.path)).deleteSync(recursive: true));
|
|
var deep = p.join(tmp.path, 'outer');
|
|
while (deep.length < 250) {
|
|
deep = p.join(deep, 'd' * 30);
|
|
}
|
|
final file = p.join(deep, 'a_file_name_that_makes_it_longer.txt');
|
|
Directory(lp(deep)).createSync(recursive: true);
|
|
File(lp(file)).writeAsStringSync('x');
|
|
expect(file.length, greaterThan(259));
|
|
expect(longPathInside(file), file);
|
|
// 실제 휴지통 함수는 JJ_TEST_REAL_RECYCLE=1 일 때만 (고장 나면 사용자 휴지통에 들어가므로)
|
|
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');
|
|
expect(longPathInside(outer), isNotNull);
|
|
if (realRecycleBin) {
|
|
expect(() => moveToRecycleBin(outer), throwsA(isA<FileSystemException>()));
|
|
expect(Directory(outer).existsSync(), isTrue);
|
|
}
|
|
final short = File(p.join(tmp.path, 'short.txt'))..writeAsStringSync('x');
|
|
expect(longPathInside(short.path), isNull, reason: '짧은 파일 경로');
|
|
});
|
|
|
|
test(r'휴지통 정보 파일 ($I) 에서 원래 경로 읽기 - Windows 10 이상 (판 2) · 예전 (판 1)', () {
|
|
List<int> le(int v, int n) => [for (var i = 0; i < n; i++) v >> (8 * i) & 0xff];
|
|
List<int> utf16(String s) => [for (final u in s.codeUnits) ...le(u, 2)];
|
|
const path = r'C:\영상\a b.mkv';
|
|
final v2 = [...le(2, 8), ...le(1234, 8), ...le(0, 8), ...le(path.length + 1, 4), ...utf16(path), 0, 0];
|
|
expect(recycledInfoPath(v2), path);
|
|
final v1 = [...le(1, 8), ...le(1234, 8), ...le(0, 8), ...utf16(path), ...List.filled(520 - path.length * 2, 0)];
|
|
expect(recycledInfoPath(v1), path);
|
|
expect(recycledInfoPath([9, 9, 9]), isNull);
|
|
expect(recycledInfoPath([...le(7, 8), ...List.filled(40, 0)]), isNull);
|
|
});
|
|
|
|
test('94: 휴지통이 없는 곳 (네트워크 공유 · 없는 드라이브) 은 휴지통으로 보내지 않는다 · 98: 이유는 읽을 수 있는 말', () {
|
|
expect(hasRecycleBin(r'\\NAS\share\a.mkv'), isFalse);
|
|
// 쓰지 않는 드라이브 문자
|
|
final unused = 'QRSTUVWXYZ'.split('').firstWhere((l) => !Directory('$l:\\').existsSync(), orElse: () => '');
|
|
if (unused.isNotEmpty) expect(hasRecycleBin('$unused:\\a.mkv'), isFalse);
|
|
expect(recycleErrorText(0x7C), contains('경로가 너무 깁니다'));
|
|
expect(recycleErrorText(0x20), contains('다른 프로그램'));
|
|
expect(recycleErrorText(0x12345), contains('0x12345'));
|
|
}, skip: !Platform.isWindows);
|
|
}
|