commit
8cce03d99c
|
|
@ -13,6 +13,7 @@ import 'support/sftp_server.dart' show sftpTestPython;
|
|||
import 'package:jj_mkvmaker/core/file_ops.dart';
|
||||
import 'package:jj_mkvmaker/core/rsync_filter.dart';
|
||||
import 'package:jj_mkvmaker/core/sync_preview.dart';
|
||||
import 'support/rsync_scenes.dart';
|
||||
|
||||
/// l (관리자 지시 10/17): FTP · FTPS (dart:io 로 직접). 실제 서버 시험은 tool/ftp_test_server.py (pyftpdlib) -
|
||||
/// 시험용 파이썬 (sftp 시험과 같은 venv) 에 pyftpdlib 가 없으면 건너뛴다
|
||||
|
|
@ -234,6 +235,24 @@ void main() {
|
|||
expect((File(real('gone/g.tmp')).existsSync(), File(real('gone/g.txt')).existsSync()), (true, false));
|
||||
}, skip: hasPy ? false : '시험용 파이썬 없음');
|
||||
|
||||
test('관리자 지시 (10/20): FTP 맞추기 결과 = 실제 rsync 결과 (28 의 장면 · 옵션 28개)', () async {
|
||||
final rsync = bundledRsync;
|
||||
if (rsync == null) return markTestSkipped('앱에 든 rsync 가 없음 (tool\\fetch_rsync.ps1)');
|
||||
await start('plain');
|
||||
FtpRegistry.configure([srv(security: 'none')]);
|
||||
final src = p.join(tmp.path, 'local', 'scene_src'), want = p.join(tmp.path, 'local', 'scene_rsync'), got = p.join(root.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, 'ftp://f1/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'));
|
||||
}, skip: hasPy ? false : '시험용 파이썬 없음', timeout: const Timeout(Duration(minutes: 5)));
|
||||
|
||||
test('동영상 스트리밍 다리: ftp:// 경로의 부분 읽기 (FtpRemote.openReadFrom - REST 로 그 자리부터)', () async {
|
||||
await start('plain');
|
||||
FtpRegistry.configure([srv(security: 'none')]);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import 'support/sftp_server.dart';
|
|||
import 'package:jj_mkvmaker/core/file_ops.dart';
|
||||
import 'package:jj_mkvmaker/core/sync_preview.dart';
|
||||
import 'package:jj_mkvmaker/core/rsync_filter.dart';
|
||||
import 'support/rsync_scenes.dart';
|
||||
|
||||
/// 88: SFTP (dartssh2). 실제 서버 시험은 tool/sftp_test_server.py (paramiko) 를 띄운다 -
|
||||
/// 파이썬 (JJ_SFTP_PY, 없으면 ..\tools\sftp_test_py 의 venv) 이 없으면 건너뛴다.
|
||||
|
|
@ -209,6 +210,24 @@ void main() {
|
|||
expect(File(real('gone/g.txt')).existsSync(), isFalse);
|
||||
}, skip: hasServer ? false : 'SFTP 시험 서버 (paramiko) 없음');
|
||||
|
||||
test('관리자 지시 (10/20): SFTP 맞추기 결과 = 실제 rsync 결과 (28 의 장면 · 옵션 28개)', () async {
|
||||
final rsync = bundledRsync;
|
||||
if (rsync == null) return markTestSkipped('앱에 든 rsync 가 없음 (tool\\fetch_rsync.ps1)');
|
||||
await start();
|
||||
SftpRegistry.configure([srv(fp: fingerprint)]);
|
||||
final src = p.join(tmp.path, 'local', 'scene_src'), want = p.join(tmp.path, 'local', 'scene_rsync'), got = p.join(root.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, 'sftp://s1/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'));
|
||||
}, skip: hasServer ? false : 'SFTP 시험 서버 (paramiko) 없음', timeout: const Timeout(Duration(minutes: 5)));
|
||||
|
||||
test('M1 (관리자 P0): SFTP 로 맞출 때 받는 쪽 같은 이름 폴더 - 지우기 없으면 그대로 · 지우기면 지우고 파일로 (미리 보기 같음)', () async {
|
||||
await start();
|
||||
SftpRegistry.configure([srv(fp: fingerprint)]);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
import 'dart:convert' show utf8;
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:jj_mkvmaker/core/sync_tools.dart' show splitOptions, toCygwinPath;
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// 28 (10/19) 이 원격 맞추기 vs 실제 rsync 비교에 쓴 장면 · 옵션 (rsync_filter_test 의 "28 의 장면" 과 같은 것).
|
||||
/// 원격 (WebDAV · SFTP · FTP · SMB) 맞추기의 결과 트리를 실제 rsync 를 로컬에서 돌린 결과 트리와 비교할 때 쓴다.
|
||||
|
||||
/// 보내는 쪽 파일
|
||||
const sceneSrc = [
|
||||
'a.txt', 'b.log', 'build/x.o', 'build/sub/y.o', 'sub/c.txt', 'sub/deep/d.txt', 'sub/deep/e.log', 'node_modules/m/i.js',
|
||||
'src/node_modules/n.js', 'docs/readme.md', 'docs/img/p.png', '한글 폴더/가.txt', 'space dir/s.bak', 'Keep.TXT', 'tmp/t.txt',
|
||||
'x/tmp/t2.txt', 'foo/bar', 'foo/q/bar',
|
||||
];
|
||||
|
||||
/// 받는 쪽에만 있는 파일 (지우기 · 거른 것 지키기 · 거른 것이 든 폴더)
|
||||
const sceneDst = [
|
||||
'extra.txt', 'keep.log', 'build/old.o', 'old/keep.log', 'old/gone.txt', 'sub/deep/z.txt', 'tmp/old.txt', 'onlydst/a.bak',
|
||||
'old2/deep/x/keep.log', 'old2/deep/x/g.txt', 'old2/deep/y/h.txt', 'old2/e.txt',
|
||||
];
|
||||
|
||||
/// 옵션 (그대로 rsync 에 · 원격 맞추기는 그 옵션의 지우기 · 거르기)
|
||||
const sceneOptions = [
|
||||
'-a --delete --exclude=*.log --exclude=build/ --exclude=/sub/deep/',
|
||||
'-a --delete --exclude=*.log',
|
||||
'-a --delete --include=*/ --include=*.txt --exclude=*',
|
||||
'-a --delete --exclude=node_modules/',
|
||||
'-a --delete --exclude=/tmp/',
|
||||
'-a --delete --exclude=tmp/',
|
||||
'-a --delete --exclude=sub/deep',
|
||||
'-a --delete --exclude=docs/***',
|
||||
'-a --delete --exclude=foo/**/bar',
|
||||
'-a --delete --exclude=**/bar',
|
||||
'-a --delete --exclude=*.[lb][oa][gk]',
|
||||
'-a --delete --exclude="space dir/"',
|
||||
'-a --delete --exclude=한글*/',
|
||||
'-a --delete --exclude=*.bak --delete-excluded',
|
||||
'-a --delete --exclude=*.log --delete-excluded',
|
||||
'-a --delete --filter="- *.o" --filter="+ build/"',
|
||||
'-a --delete --include=sub/deep/d.txt --exclude=sub/deep/*',
|
||||
'-a --delete --exclude=?.txt',
|
||||
'-a --delete --exclude=*.TXT',
|
||||
'-a --delete --exclude=deep/',
|
||||
'-a --delete --exclude=/old2/deep/x/keep.log',
|
||||
'-a --del',
|
||||
'-a --delete-after --exclude=*.log',
|
||||
'-a --delete --exclude=**/x/',
|
||||
'-a --delete --include=*.txt --exclude=*.log --exclude=*/',
|
||||
'-a --delete -f"+ *.txt" --exclude=*',
|
||||
'-a --delete -f"- *.log"',
|
||||
// 지우기 없음 (거른 것만)
|
||||
'-a --exclude=*.log --exclude=build/',
|
||||
];
|
||||
|
||||
/// 앱에 들어 있는 rsync (Windows). 없으면 null
|
||||
String? get bundledRsync {
|
||||
final f = p.join(Directory.current.path, 'third_party', 'rsync', 'windows', 'rsync.exe');
|
||||
return Platform.isWindows && File(f).existsSync() ? f : null;
|
||||
}
|
||||
|
||||
/// 장면을 만든다 (있던 것은 지우고). 보내는 쪽 글은 "s:경로", 받는 쪽은 "d:경로"
|
||||
void writeScene(String src, String dst) {
|
||||
for (final (dir, list, tag) in [(src, sceneSrc, 's'), (dst, sceneDst, 'd')]) {
|
||||
final d = Directory(dir);
|
||||
if (d.existsSync()) d.deleteSync(recursive: true);
|
||||
d.createSync(recursive: true);
|
||||
for (final r in list) {
|
||||
File(p.join(dir, r))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('$tag:$r');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 폴더 안의 트리: 상대 경로 (/) → 폴더면 "/", 파일이면 그 글
|
||||
Map<String, String> treeOf(String dir) {
|
||||
final out = <String, String>{};
|
||||
final root = Directory(dir);
|
||||
if (!root.existsSync()) return out;
|
||||
for (final e in root.listSync(recursive: true)) {
|
||||
final rel = p.relative(e.path, from: dir).replaceAll(r'\', '/');
|
||||
out[rel] = e is Directory ? '/' : File(e.path).readAsStringSync();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// 실제 rsync 로 src/ → dst/ 를 맞춘 뒤 dst 의 트리 (src · dst 는 writeScene 으로 만든 로컬 폴더)
|
||||
Future<Map<String, String>> realRsyncTree(String rsync, String src, String dst, String opts) async {
|
||||
final r = await Process.run(rsync, ['-8', ...splitOptions(opts), '${toCygwinPath(src)}/', '${toCygwinPath(dst)}/'],
|
||||
stdoutEncoding: utf8, stderrEncoding: utf8);
|
||||
if (r.exitCode != 0) throw StateError('rsync $opts → ${r.exitCode}: ${r.stderr}');
|
||||
return treeOf(dst);
|
||||
}
|
||||
|
||||
/// 두 트리의 다른 점 (없으면 빈 목록)
|
||||
List<String> treeDiff(Map<String, String> want, Map<String, String> got) => [
|
||||
for (final k in {...want.keys, ...got.keys}.toList()..sort())
|
||||
if (want[k] != got[k])
|
||||
'$k: rsync=${want[k] == null ? '없음' : want[k] == '/' ? '폴더' : '"${want[k]}"'} · 앱=${got[k] == null ? '없음' : got[k] == '/' ? '폴더' : '"${got[k]}"'}',
|
||||
];
|
||||
Loading…
Reference in New Issue