392 lines
22 KiB
Dart
392 lines
22 KiB
Dart
import 'dart:io';
|
||
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||
import 'package:jj_mkvmaker/app/live_sync.dart';
|
||
import 'package:jj_mkvmaker/app/settings.dart';
|
||
import 'package:jj_mkvmaker/core/file_ops.dart';
|
||
import 'package:jj_mkvmaker/core/playlist.dart' show PlaylistMode;
|
||
import 'package:jj_mkvmaker/core/vfs.dart';
|
||
import 'package:jj_mkvmaker/core/webdav.dart';
|
||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||
import 'package:path/path.dart' as p;
|
||
|
||
import 'support/dav_server.dart';
|
||
import 'package:jj_mkvmaker/core/sync_preview.dart';
|
||
import 'package:jj_mkvmaker/core/rsync_filter.dart';
|
||
import 'support/rsync_scenes.dart';
|
||
|
||
void main() {
|
||
late Directory tmp, remote, local;
|
||
late TestDavServer server;
|
||
const id = 'nas';
|
||
|
||
setUp(() async {
|
||
tmp = Directory.systemTemp.createTempSync('jj_dav_');
|
||
remote = Directory(p.join(tmp.path, 'remote'))..createSync();
|
||
local = Directory(p.join(tmp.path, 'local'))..createSync();
|
||
server = TestDavServer(remote);
|
||
await server.start();
|
||
DavRegistry.configure([DavServer(id: id, name: '집 NAS', url: server.url, user: 'user', password: 'pass')]);
|
||
});
|
||
tearDown(() async {
|
||
DavRegistry.configure([]);
|
||
await server.stop();
|
||
tmp.deleteSync(recursive: true);
|
||
});
|
||
|
||
File lf(String rel, [String text = 'x']) => File(p.join(local.path, rel))
|
||
..parent.createSync(recursive: true)
|
||
..writeAsStringSync(text);
|
||
File rf(String rel, [String text = 'x']) => File(p.join(remote.path, rel))
|
||
..parent.createSync(recursive: true)
|
||
..writeAsStringSync(text);
|
||
|
||
test('경로: dav://서버/경로 정리 · 이어 붙이기 · 같은지 · 안에 있는지', () {
|
||
expect(DavPath.parse(r'dav://nas/a\b/').full, 'dav://nas/a/b');
|
||
expect(DavPath.parse('dav://nas').full, 'dav://nas/');
|
||
expect(vJoin('dav://nas/', '한글 폴더'), 'dav://nas/한글 폴더');
|
||
expect(vDirname('dav://nas/a/b'), 'dav://nas/a');
|
||
expect(vDirname('dav://nas/a'), 'dav://nas/');
|
||
expect(vBasename('dav://nas/a/b.mkv'), 'b.mkv');
|
||
expect(samePath('dav://nas/a/', r'dav://nas/a'), isTrue);
|
||
expect(isSameOrInside('dav://nas/a/b', 'dav://nas/a'), isTrue);
|
||
expect(isSameOrInside('dav://nas/ab', 'dav://nas/a'), isFalse);
|
||
expect(isSameOrInside('dav://nas/a', 'dav://other/a'), isFalse);
|
||
expect(samePath('dav://nas/a', r'C:\a'), isFalse);
|
||
});
|
||
|
||
test('목록 · 폴더 만들기 · 이름 바꾸기 · 지우기 (한글 이름 · 인증)', () async {
|
||
rf('영상/a.mkv', 'aaaa');
|
||
rf('영상/sub/b.txt', 'b');
|
||
final top = await listEntries('dav://$id/');
|
||
expect(top.map((e) => (e.name, e.isDir)), [('영상', true)]);
|
||
final inside = sortEntries(await listEntries('dav://$id/영상'), SortBy.name);
|
||
expect(inside.map((e) => (e.name, e.isDir, e.size)), [('sub', true, 0), ('a.mkv', false, 4)]);
|
||
final made = await FileOps.makeFolder('dav://$id/영상', '새 폴더');
|
||
expect(made, 'dav://$id/영상/새 폴더');
|
||
expect(Directory(p.join(remote.path, '영상', '새 폴더')).existsSync(), isTrue);
|
||
final renamed = await FileOps.rename('dav://$id/영상/a.mkv', 'c.mkv');
|
||
expect(renamed, 'dav://$id/영상/c.mkv');
|
||
expect(File(p.join(remote.path, '영상', 'c.mkv')).readAsStringSync(), 'aaaa');
|
||
await FileOps().delete(['dav://$id/영상/sub']);
|
||
expect(Directory(p.join(remote.path, '영상', 'sub')).existsSync(), isFalse);
|
||
// 잘못된 비밀번호
|
||
DavRegistry.configure([DavServer(id: id, name: 'x', url: server.url, user: 'user', password: 'wrong')]);
|
||
expect(() => listEntries('dav://$id/'), throwsA(isA<DavException>().having((e) => e.status, 'status', 401)));
|
||
});
|
||
|
||
test('복사 · 이동: 로컬 → WebDAV · WebDAV → 로컬 · 같은 서버 안 이동 (속도 알림)', () async {
|
||
lf('up/x.txt', 'xx');
|
||
lf('up/deep/y.bin', 'yyy');
|
||
var bytes = 0;
|
||
final ops = FileOps(onBytes: (n) => bytes += n);
|
||
expect(await ops.copy([p.join(local.path, 'up')], 'dav://$id/'), ['dav://$id/up']);
|
||
expect(File(p.join(remote.path, 'up', 'deep', 'y.bin')).readAsStringSync(), 'yyy');
|
||
expect(bytes, 5);
|
||
// 같은 이름이면 "이름 (2)"
|
||
expect(await ops.copy([p.join(local.path, 'up')], 'dav://$id/'), ['dav://$id/up (2)']);
|
||
// WebDAV → 로컬
|
||
final down = Directory(p.join(local.path, 'down'))..createSync();
|
||
await ops.copy(['dav://$id/up'], down.path);
|
||
expect(File(p.join(down.path, 'up', 'x.txt')).readAsStringSync(), 'xx');
|
||
// 같은 서버 안 이동 (서버가 MOVE)
|
||
await FileOps.makeFolder('dav://$id/', 'box');
|
||
await ops.move(['dav://$id/up (2)'], 'dav://$id/box');
|
||
expect(Directory(p.join(remote.path, 'box', 'up (2)')).existsSync(), isTrue);
|
||
expect(Directory(p.join(remote.path, 'up (2)')).existsSync(), isFalse);
|
||
expect(server.requests.where((r) => r.startsWith('MOVE')), isNotEmpty);
|
||
// 로컬 → WebDAV 이동 (복사 후 원본 지움)
|
||
lf('mv/m.txt', 'm');
|
||
await ops.move([p.join(local.path, 'mv')], 'dav://$id/box');
|
||
expect(File(p.join(remote.path, 'box', 'mv', 'm.txt')).readAsStringSync(), 'm');
|
||
expect(Directory(p.join(local.path, 'mv')).existsSync(), isFalse);
|
||
});
|
||
|
||
test('동기화 (mirror): 바뀐 것만 · 지우기 · -u · 원격 → 로컬도', () async {
|
||
lf('src/a.txt', 'a');
|
||
lf('src/sub/b.txt', 'b');
|
||
final ops = FileOps();
|
||
expect(await ops.mirror(p.join(local.path, 'src'), 'dav://$id/dst'), 2);
|
||
expect(File(p.join(remote.path, 'dst', 'sub', 'b.txt')).readAsStringSync(), 'b');
|
||
// 다시: 바뀐 것 없음 (올린 파일은 서버 시각이 원본보다 새것)
|
||
expect(await ops.mirror(p.join(local.path, 'src'), 'dav://$id/dst'), 0);
|
||
// 원본에서 내용 (크기) 이 바뀐 것만
|
||
lf('src/a.txt', 'aaa');
|
||
expect(await ops.mirror(p.join(local.path, 'src'), 'dav://$id/dst'), 1);
|
||
expect(File(p.join(remote.path, 'dst', 'a.txt')).readAsStringSync(), 'aaa');
|
||
// 지우기 포함: 대상에만 있는 것
|
||
rf('dst/extra.txt', 'e');
|
||
await ops.mirror(p.join(local.path, 'src'), 'dav://$id/dst', delete: true);
|
||
expect(File(p.join(remote.path, 'dst', 'extra.txt')).existsSync(), isFalse);
|
||
// -u: 대상이 더 새것이면 덮어쓰지 않음
|
||
rf('dst/a.txt', 'remote newer!!');
|
||
final old = DateTime.now().subtract(const Duration(days: 1));
|
||
File(p.join(local.path, 'src', 'a.txt')).setLastModifiedSync(old);
|
||
await ops.mirror(p.join(local.path, 'src'), 'dav://$id/dst', update: true);
|
||
expect(File(p.join(remote.path, 'dst', 'a.txt')).readAsStringSync(), 'remote newer!!');
|
||
// 원격 → 로컬 (받은 파일은 원격의 바뀐 때로 맞춰져 다시 받지 않음)
|
||
final back = p.join(local.path, 'back');
|
||
expect(await ops.mirror('dav://$id/dst', back), 2);
|
||
expect(File(p.join(back, 'sub', 'b.txt')).readAsStringSync(), 'b');
|
||
expect(await ops.mirror('dav://$id/dst', back), 0);
|
||
});
|
||
|
||
test('빈 폴더 지우기 · 파일 수 · 용량 · 임시로 받기', () async {
|
||
rf('p/e1/e2/.keep', '');
|
||
File(p.join(remote.path, 'p', 'e1', 'e2', '.keep')).deleteSync();
|
||
rf('p/f/z.txt', 'z');
|
||
expect(await vCountFiles('dav://$id/p'), 1);
|
||
expect(await removeEmptyDirs('dav://$id/p'), 2); // e1/e2, e1
|
||
expect(Directory(p.join(remote.path, 'p', 'f')).existsSync(), isTrue);
|
||
expect(await DavRegistry.client(id).quota(), (1000000, 500));
|
||
final got = await vLocalCopy('dav://$id/p/f/z.txt', p.join(local.path, 'cache'));
|
||
expect(File(got).readAsStringSync(), 'z');
|
||
expect(transferProblem(['dav://$id/p'], 'dav://$id/p/f', move: false), contains('자기 자신 안으로'));
|
||
});
|
||
|
||
test('실시간 동기화 (lsync): 로컬 → WebDAV 쌍은 rsync 로 정해 두어도 앱이 맞추고 남은 것 · 지우기', () async {
|
||
lf('live/a.txt', 'a');
|
||
lf('live/sub/b.txt', 'bb');
|
||
rf('mirror/old.txt', 'o');
|
||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||
// 예전에 확인한 쌍은 처음 맞출 때 지금 옵션을 기억하며 설정을 저장한다 (서버 목록도 설정에서 다시 읽음) - 시험 서버를 설정에도
|
||
c.settings.remote.webdavServers = [DavServer(id: id, name: '집 NAS', url: server.url, user: 'user', password: 'pass')];
|
||
final pair = LiveSyncPair(p.join(local.path, 'live'), 'dav://$id/mirror', method: 'rsync', delete: true, deleteConfirmed: true);
|
||
c.settings.copy.liveSyncPairs = [pair];
|
||
final live = LiveSync(c);
|
||
expect(await LiveSync.diff(pair), unorderedEquals(['a.txt', 'sub/b.txt', '− old.txt']));
|
||
await live.syncNow(pair);
|
||
expect(live.status[LiveSync.keyOf(pair)]!.$2, isNot(contains('실패')));
|
||
expect(File(p.join(remote.path, 'mirror', 'sub', 'b.txt')).readAsStringSync(), 'bb');
|
||
expect(File(p.join(remote.path, 'mirror', 'old.txt')).existsSync(), isFalse);
|
||
expect(live.pending[LiveSync.keyOf(pair)], isEmpty);
|
||
live.dispose();
|
||
});
|
||
|
||
test('스트리밍 재생: 주소 · 인증 헤더, 같은 폴더 동영상 목록 · 자막 (받지 않고)', () async {
|
||
rf('영상/1화.mkv', 'v1');
|
||
rf('영상/2화.mkv', 'v2');
|
||
rf('영상/1화.ko.srt', '1\n00:00:01,000 --> 00:00:02,000\n안녕\n');
|
||
rf('영상/메모.txt', 't');
|
||
final s = davStream('dav://$id/영상/1화.mkv');
|
||
expect(s.url, '${server.url}/${Uri.encodeComponent('영상')}/${Uri.encodeComponent('1화.mkv')}');
|
||
expect(s.headers['Authorization'], startsWith('Basic '));
|
||
// 55: 다른 앱으로 넘기는 주소에는 아이디 · 비밀번호를 넣지 않는다
|
||
expect(vPlayable('dav://$id/영상/1화.mkv'), s.url);
|
||
expect(vPlayable('dav://$id/영상/1화.mkv'), isNot(contains('pass')));
|
||
expect(vPlayable(r'C:\a.mkv'), r'C:\a.mkv');
|
||
// 주소 + 헤더로 실제로 받을 수 있다 (플레이어와 같은 요청)
|
||
final http = HttpClient();
|
||
final req = await http.getUrl(Uri.parse(s.url));
|
||
s.headers.forEach(req.headers.set);
|
||
final res = await req.close();
|
||
expect(await res.transform(const SystemEncoding().decoder).join(), 'v1');
|
||
http.close();
|
||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||
c.settings.playback.playlistMode = PlaylistMode.folder;
|
||
final before = server.requests.where((r) => r.startsWith('GET')).length;
|
||
final plan = await c.preparePlayback(['dav://$id/영상/1화.mkv']);
|
||
expect(plan!.$1, ['dav://$id/영상/1화.mkv', 'dav://$id/영상/2화.mkv']);
|
||
expect(server.requests.where((r) => r.startsWith('GET')).length, before); // 동영상은 받지 않음
|
||
final subs = await c.externalSubtitlesFor('dav://$id/영상/1화.mkv');
|
||
expect(subs.map(p.basename), ['1화.ko.srt']);
|
||
expect(File(subs.single).readAsStringSync(), contains('안녕'));
|
||
});
|
||
test('M1 (관리자 P0): 원본 파일 · 받는 쪽 같은 이름 폴더 - 지우기 없으면 폴더를 지우지 않고 건너뜀, 지우기면 지우고 파일로 · 미리 보기도 같음', () async {
|
||
lf('src/x', 'file');
|
||
lf('src/ok.txt', 'ok');
|
||
rf('dst/x/keep.txt', 'k');
|
||
rf('dst/x/sub/deep.txt', 'd');
|
||
final src = p.join(local.path, 'src');
|
||
const dst = 'dav://$id/dst';
|
||
// 지우기 없음: 미리 보기 "건너뜀" · 지울 것 없음 → 실제로도 폴더 그대로
|
||
var pv = await previewSync(src, dst, toRight: true);
|
||
expect(pv.where((x) => x.action == PreviewAction.skipDir).map((x) => x.rel), ['x']);
|
||
expect(pv.where((x) => x.action == PreviewAction.delete), isEmpty);
|
||
final ops = FileOps();
|
||
await ops.mirror(src, dst);
|
||
expect(File(p.join(remote.path, 'dst', 'x', 'keep.txt')).readAsStringSync(), 'k', reason: '예전에는 이 폴더를 통째로 지웠다');
|
||
expect(File(p.join(remote.path, 'dst', 'x', 'sub', 'deep.txt')).existsSync(), isTrue);
|
||
expect(File(p.join(remote.path, 'dst', 'ok.txt')).readAsStringSync(), 'ok', reason: '다른 것은 그대로 맞춤');
|
||
expect(ops.dirConflicts.map(vBasename), ['x']);
|
||
// 지우기 포함: 미리 보기에 "폴더 · 안의 3개를 지우고 파일로" → 실제로 폴더가 파일로
|
||
pv = await previewSync(src, dst, toRight: true, delete: true);
|
||
final del = pv.singleWhere((x) => x.action == PreviewAction.delete);
|
||
expect((del.rel, del.isDir, del.replacedByFile, del.inner), ('x', true, true, 3));
|
||
final ops2 = FileOps();
|
||
await ops2.mirror(src, dst, delete: true);
|
||
expect(File(p.join(remote.path, 'dst', 'x')).readAsStringSync(), 'file');
|
||
expect(ops2.dirConflicts, isEmpty);
|
||
});
|
||
/// 관리자 지시 (10/19): 원격 맞추기 (_mirrorV) 가 rsync 옵션의 거르기를 미리 보기와 같게 따르는지 -
|
||
/// 미리 보기의 "새로 · 바뀜" 은 실제로 생기고, "지워짐" 은 실제로 없어지고, 그 밖의 받는 쪽 것은 그대로
|
||
Future<void> checkSame(String src, String dst, String Function(String rel) real, String options, {required bool delete}) async {
|
||
final filter = RsyncFilter.parse(options);
|
||
final before = <String>{};
|
||
void scan(Directory d, String rel) {
|
||
for (final e in d.listSync()) {
|
||
final r = rel.isEmpty ? p.basename(e.path) : '$rel/${p.basename(e.path)}';
|
||
before.add(r);
|
||
if (e is Directory) scan(e, r);
|
||
}
|
||
}
|
||
final root = Directory(real(''));
|
||
if (root.existsSync()) scan(root, '');
|
||
final pv = await previewSync(src, dst, toRight: true, delete: delete, filter: filter);
|
||
await FileOps().mirror(src, dst, delete: delete, filter: filter);
|
||
for (final x in pv) {
|
||
final there = FileSystemEntity.typeSync(real(x.rel)) != FileSystemEntityType.notFound;
|
||
switch (x.action) {
|
||
case PreviewAction.add || PreviewAction.update:
|
||
expect(there, isTrue, reason: '미리 보기 + / ~ ${x.rel} → 실제로 있음 ($options)');
|
||
case PreviewAction.delete when !x.replacedByFile:
|
||
expect(there, isFalse, reason: '미리 보기 − ${x.rel} → 실제로 지워짐 ($options)');
|
||
default:
|
||
}
|
||
}
|
||
final shownDeleted = {for (final x in pv) if (x.action == PreviewAction.delete) x.rel};
|
||
for (final r in before) {
|
||
final gone = FileSystemEntity.typeSync(real(r)) == FileSystemEntityType.notFound;
|
||
final underShown = shownDeleted.any((d) => r == d || r.startsWith('$d/'));
|
||
if (gone) expect(underShown, isTrue, reason: '실제로 지워진 $r 는 미리 보기 지울 목록에 ($options)');
|
||
}
|
||
}
|
||
|
||
test('관리자 지시 (10/19): WebDAV 맞추기가 거르기 (--exclude · --include · --filter · --delete-excluded) 를 따르고 미리 보기와 같음', () async {
|
||
void fresh() {
|
||
for (final d in [Directory(p.join(local.path, 'src')), Directory(p.join(remote.path, 'dst'))]) {
|
||
if (d.existsSync()) d.deleteSync(recursive: true);
|
||
}
|
||
lf('src/a.txt', 'a');
|
||
lf('src/skip.tmp', 's');
|
||
lf('src/cache/c.bin', 'c');
|
||
lf('src/keep/x.txt', 'x');
|
||
lf('src/keep/y.log', 'y');
|
||
lf('src/한글 폴더/가.txt', '가');
|
||
rf('dst/only.tmp', 'o'); // 받는 쪽에만 있고 거른 것
|
||
rf('dst/old.txt', 'old'); // 받는 쪽에만 있음
|
||
rf('dst/cache/remote.bin', 'r'); // 거른 폴더
|
||
rf('dst/gone/g.tmp', 'g'); // 받는 쪽에만 있는 폴더 안의 거른 것
|
||
rf('dst/gone/g.txt', 'g');
|
||
}
|
||
|
||
final src = p.join(local.path, 'src');
|
||
const dst = 'dav://$id/dst';
|
||
String real(String rel) => p.joinAll([remote.path, 'dst', ...rel.split('/').where((s) => s.isNotEmpty)]);
|
||
for (final (opts, delete) in [
|
||
('-av --exclude=*.tmp --exclude=cache/', true),
|
||
('-av --exclude=*.tmp --exclude=cache/ --delete-excluded', true),
|
||
('-av --include=keep/ --include=keep/*.txt --exclude=keep/* --exclude=*.tmp', true),
|
||
('-av --filter="- *.log" --filter="- /cache"', true),
|
||
('-av --exclude=*.tmp', false),
|
||
]) {
|
||
fresh();
|
||
await checkSame(src, dst, real, opts, delete: delete);
|
||
}
|
||
// 하나를 자세히: 거른 것은 보내지 않고 받는 쪽의 거른 것은 지키며, 거른 것이 든 폴더는 남기고 안의 것만 지움
|
||
fresh();
|
||
await FileOps().mirror(src, dst, delete: true, filter: RsyncFilter.parse('--exclude=*.tmp --exclude=cache/'));
|
||
expect(File(real('a.txt')).existsSync(), isTrue);
|
||
expect(File(real('skip.tmp')).existsSync(), isFalse, reason: '거른 것은 보내지 않음');
|
||
expect(File(real('cache/c.bin')).existsSync(), isFalse, reason: '거른 폴더 안으로 내려가지 않음');
|
||
expect(File(real('only.tmp')).readAsStringSync(), 'o', reason: '받는 쪽의 거른 것은 지키기');
|
||
expect(File(real('cache/remote.bin')).existsSync(), isTrue);
|
||
expect(File(real('old.txt')).existsSync(), isFalse, reason: '거르지 않은 것은 --delete 로 지움');
|
||
expect(File(real('gone/g.tmp')).existsSync(), isTrue, reason: '거른 것이 든 폴더는 남기고');
|
||
expect(File(real('gone/g.txt')).existsSync(), isFalse, reason: '그 안의 거르지 않은 것만 지움');
|
||
expect(File(real('한글 폴더/가.txt')).readAsStringSync(), '가');
|
||
// --delete-excluded: 받는 쪽의 거른 것도 지움
|
||
fresh();
|
||
await FileOps().mirror(src, dst, delete: true, filter: RsyncFilter.parse('--exclude=*.tmp --exclude=cache/ --delete-excluded'));
|
||
expect(File(real('only.tmp')).existsSync(), isFalse);
|
||
expect(Directory(real('cache')).existsSync(), isFalse);
|
||
expect(Directory(real('gone')).existsSync(), isFalse);
|
||
});
|
||
test('관리자 지시 (10/20): WebDAV 맞추기 결과 = 실제 rsync 결과 (28 의 장면 · 옵션 28개)', () async {
|
||
final rsync = sceneRsync;
|
||
if (rsync == null) return markTestSkipped('앱에 든 rsync 가 없음 (tool\\fetch_rsync.ps1)');
|
||
final src = p.join(local.path, 'scene_src'), want = p.join(local.path, 'scene_rsync'), got = p.join(remote.path, 'scene');
|
||
final bad = <String>[];
|
||
for (final opts in sceneOptions) {
|
||
writeScene(src, want);
|
||
final w = await realRsyncTree(rsync, src, want, opts);
|
||
writeScene(src, got);
|
||
await FileOps().mirror(src, 'dav://$id/scene', delete: optionsDelete(opts), filter: RsyncFilter.parse(opts));
|
||
final d = treeDiff(w, treeOf(got));
|
||
if (d.isNotEmpty) bad.add('$opts\n ${d.join('\n ')}');
|
||
}
|
||
expect(bad, isEmpty, reason: bad.join('\n'));
|
||
}, timeout: const Timeout(Duration(minutes: 5)));
|
||
|
||
test('관리자 지시 (10/20): 실시간 동기화 (WebDAV rsync 쌍) 결과 = 실제 rsync 결과 · 지울지 묻는 목록 = rsync 가 지우는 것 (28 의 장면 · 옵션 28개)', () async {
|
||
final rsync = sceneRsync;
|
||
if (rsync == null) return markTestSkipped('앱에 든 rsync 가 없음 (tool\\fetch_rsync.ps1)');
|
||
final src = p.join(local.path, 'live_src'), want = p.join(local.path, 'live_rsync'), got = p.join(remote.path, 'live');
|
||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||
// 지우기를 정하면 설정을 저장하며 서버 목록을 설정에서 다시 읽는다 - 시험 서버를 설정에도
|
||
c.settings.remote.webdavServers = [DavServer(id: id, name: '집 NAS', url: server.url, user: 'user', password: 'pass')];
|
||
final bad = <String>[];
|
||
for (final opts in sceneOptions) {
|
||
writeScene(src, want);
|
||
final w = await realRsyncTree(rsync, src, want, opts);
|
||
writeScene(src, got);
|
||
final before = treeOf(got);
|
||
// rsync 가 지운 것 (받는 쪽에 있다가 없어진 것)
|
||
final gone = [for (final k in before.keys) if (!w.containsKey(k)) k];
|
||
c.settings.copy.rsyncOptions = opts;
|
||
// 새 쌍 (지우기 확인 전) - 지울 것이 있으면 지우지 않고 맞춘 뒤 [toDelete] 로 묻는다
|
||
final pair = LiveSyncPair(src, 'dav://$id/live', method: 'rsync', delete: optionsDelete(opts));
|
||
c.settings.copy.liveSyncPairs = [pair];
|
||
final live = LiveSync(c);
|
||
final k = LiveSync.keyOf(pair);
|
||
final why = <String>[];
|
||
await live.syncNow(pair);
|
||
if (live.status[k]!.$2.contains('실패')) why.add('상태: ${live.status[k]!.$2}');
|
||
final asked = live.toDelete[k] ?? const <String>[];
|
||
// 묻는 목록 = rsync 가 지우는 것 (폴더는 한 줄로 - 그 안의 것은 그 폴더에 든 것으로)
|
||
for (final g in gone) {
|
||
if (!asked.any((a) => g == a || g.startsWith('$a/'))) why.add('rsync 는 지우는데 묻지 않음: $g');
|
||
}
|
||
for (final a in asked) {
|
||
if (!gone.contains(a)) why.add('rsync 는 지우지 않는데 물음: $a');
|
||
}
|
||
if (asked.isNotEmpty) await live.decideDelete(c.settings.copy.liveSyncPairs.single, delete: true);
|
||
why.addAll(treeDiff(w, treeOf(got)));
|
||
if ((live.pending[k] ?? const []).isNotEmpty) why.add('맞춘 뒤에도 맞출 것: ${live.pending[k]}');
|
||
live.dispose();
|
||
if (why.isNotEmpty) bad.add('$opts\n ${why.join('\n ')}');
|
||
}
|
||
expect(bad, isEmpty, reason: bad.join('\n'));
|
||
}, timeout: const Timeout(Duration(minutes: 5)));
|
||
|
||
test('관리자 3a 판단 (10/20): 원격 (WebDAV) 쌍도 확인한 뒤 옵션이 바뀌어 지울 것이 늘면 다시 묻는다', () async {
|
||
lf('rq/a.txt', 'a');
|
||
rf('rqd/only.tmp', 'o');
|
||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||
c.settings
|
||
..remote.webdavServers = [DavServer(id: id, name: '집 NAS', url: server.url, user: 'user', password: 'pass')]
|
||
..copy.rsyncOptions = '-av --exclude=*.tmp'
|
||
..copy.liveSyncPairs = [
|
||
LiveSyncPair(p.join(local.path, 'rq'), 'dav://$id/rqd',
|
||
method: 'rsync', delete: true, deleteConfirmed: true, confirmedOptions: '-av --exclude=*.tmp'),
|
||
];
|
||
final live = LiveSync(c);
|
||
addTearDown(live.dispose);
|
||
LiveSyncPair pair() => c.settings.copy.liveSyncPairs.single;
|
||
final k = LiveSync.keyOf(pair());
|
||
await live.syncNow(pair());
|
||
expect(live.toDelete[k], isNull);
|
||
c.settings.copy.rsyncOptions = '-av --exclude=*.tmp --delete-excluded';
|
||
await live.syncNow(pair());
|
||
expect(live.toDelete[k], ['only.tmp']);
|
||
expect(live.reasked, contains(k));
|
||
expect(File(p.join(remote.path, 'rqd', 'only.tmp')).existsSync(), isTrue, reason: '확인 전에는 지우지 않음');
|
||
await live.decideDelete(pair(), delete: true);
|
||
expect(File(p.join(remote.path, 'rqd', 'only.tmp')).existsSync(), isFalse);
|
||
expect(pair().confirmedOptions, '-av --exclude=*.tmp --delete-excluded');
|
||
});
|
||
}
|