JJ_MKVMaker/test/webdav_test.dart

221 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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';
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()));
final pair = LiveSyncPair(p.join(local.path, 'live'), 'dav://$id/mirror', method: 'rsync', delete: true, deleteConfirmed: true);
c.settings.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.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);
});
}