P0 Windows rsync: 인수를 응답 파일 (@) 로 넘기고 MSYS=noglob - ' 짝짓기 · 글로브 펼치기로 거르기가 사라져 지킬 파일이 지워지던 것 (검토 대기)
관리자 지시 (10/20, 28 재현 - 데이터, 정식 판에도 있음): Windows 의 rsync (MSYS2 런타임, 3.5.1) 는 Windows 프로그램이 넘긴
명령 줄을 스스로 다시 읽는다. 앱의 인수는 맞는데:
- ' 를 따옴표로 짝지어 인수를 합침: -a --delete --exclude="it's.txt" --exclude="Bob's.txt" 가 한 인수가 되어 거르기가 사라지고
it's.txt · Bob's.txt 가 지워짐 (미리 보기는 "남음"). ' 하나면 rsync 가 실패, ' 가 든 경로 (한쪽 · 양쪽) 는 합쳐져 실패.
- * ? [ ] { } 를 작업 폴더의 파일로 펼침: --exclude *.log 가 작업 폴더의 x.log y.log 로 바뀌어 거르기가 빗나감.
실험 (실제 rsync): MSYS=noglob 은 글로브만 막고 ' 짝짓기는 그대로 (CYGWIN=noglob 은 소용없음). Dart 는 빈칸 · 큰따옴표가 없는
인수를 따옴표로 감싸지 않고, 감쌀 때의 \" 를 MSYS 는 다르게 읽어 명령 줄로는 고칠 수 없음. 응답 파일에 인수마다 큰따옴표로 감싸
넣고 (안의 \ 는 그대로) "@args.txt" 로 넘기면 ' · * ? [ ] { } · 빈칸 · 한글 · \ · @ 가 모두 그대로 감. 응답 파일 경로에 빈칸 · ' 가
있으면 @ 가 먹히지 않아 그 폴더를 작업 폴더로 하고 상대 이름으로 넘김.
- lib/core/rsync_launch.dart: prepareRsyncLaunch (Windows 만 - Android 는 그대로) · 끝나면 지움. 기존 MSYS 값에 noglob 을 더함.
- 띄우는 곳 둘 다: TransferJob (복사 · Rsync 화면 · 모니터링 [실행] · 최종 정리 - robocopy 는 그대로) · LiveSync (실시간 동기화).
미리 보기는 rsync 를 쓰지 않는다.
- 큰따옴표 (") 가 든 인수는 응답 파일로도 그대로 못 넘김 (Windows 파일 이름에는 없음): 옵션 칸 · 확인 창 · 모니터링에 빨갛게
(unreadableOptions, Windows 만) · 띄울 때도 막고 이유를 보임.
- 알아 둘 것: rsync 의 작업 폴더가 응답 파일 폴더가 되어, 옵션의 상대 경로 (--exclude-from=목록.txt) 는 그 폴더 기준 (예전에도
앱의 작업 폴더라 정해지지 않았음 - 절대 경로를 쓰는 것이 맞음).
시험 rsync_msys_args_test (실제 rsync): ' 두 개 · 하나의 --exclude, ' 가 든 원본 경로 · 원본 · 대상 경로 · 경로 + exclude, 따로 쓴 *
· ? · [ ] · { }, [ ] · { } · 빈칸 · 한글 경로, 실시간 동기화 쌍, 응답 파일 지우기, 도우미 · 큰따옴표 막기. 고치기 전에는 7개 실패.
거르기 · 맞추기 · 실시간 · 복사 시험 25개 파일 · rsync_page_test (Windows 통합) 통과.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
next9
parent
f45ad3b25f
commit
716057734a
|
|
@ -2598,6 +2598,8 @@
|
||||||
"큰 파일": "Large files",
|
"큰 파일": "Large files",
|
||||||
"큰 파일이 없습니다": "No large files",
|
"큰 파일이 없습니다": "No large files",
|
||||||
"큰 폴더": "Large folders",
|
"큰 폴더": "Large folders",
|
||||||
|
"큰따옴표 (\") 가 든 옵션은 Windows 의 rsync 에 넘길 수 없습니다: {0}": "Options containing a double quote (\") cannot be passed to rsync on Windows: {0}",
|
||||||
|
"큰따옴표 (\") 가 든 인수는 Windows 의 rsync 에 그대로 넘길 수 없어 실행하지 않았습니다: {0}": "Arguments containing a double quote (\") cannot be passed as-is to rsync on Windows, so it was not run: {0}",
|
||||||
"키 있음": "Key saved",
|
"키 있음": "Key saved",
|
||||||
"타갈로그어": "Tagalog",
|
"타갈로그어": "Tagalog",
|
||||||
"타밀어": "Tamil",
|
"타밀어": "Tamil",
|
||||||
|
|
|
||||||
|
|
@ -2598,6 +2598,8 @@
|
||||||
"큰 파일": "大きいファイル",
|
"큰 파일": "大きいファイル",
|
||||||
"큰 파일이 없습니다": "大きいファイルはありません",
|
"큰 파일이 없습니다": "大きいファイルはありません",
|
||||||
"큰 폴더": "大きいフォルダー",
|
"큰 폴더": "大きいフォルダー",
|
||||||
|
"큰따옴표 (\") 가 든 옵션은 Windows 의 rsync 에 넘길 수 없습니다: {0}": "ダブルクォート (\") を含むオプションは Windows の rsync に渡せません: {0}",
|
||||||
|
"큰따옴표 (\") 가 든 인수는 Windows 의 rsync 에 그대로 넘길 수 없어 실행하지 않았습니다: {0}": "ダブルクォート (\") を含む引数は Windows の rsync にそのまま渡せないため、実行しませんでした: {0}",
|
||||||
"키 있음": "キーあり",
|
"키 있음": "キーあり",
|
||||||
"타갈로그어": "タガログ語",
|
"타갈로그어": "タガログ語",
|
||||||
"타밀어": "タミル語",
|
"타밀어": "タミル語",
|
||||||
|
|
|
||||||
|
|
@ -2598,6 +2598,8 @@
|
||||||
"큰 파일": "大文件",
|
"큰 파일": "大文件",
|
||||||
"큰 파일이 없습니다": "没有大文件",
|
"큰 파일이 없습니다": "没有大文件",
|
||||||
"큰 폴더": "大文件夹",
|
"큰 폴더": "大文件夹",
|
||||||
|
"큰따옴표 (\") 가 든 옵션은 Windows 의 rsync 에 넘길 수 없습니다: {0}": "含有双引号 (\") 的选项无法传给 Windows 上的 rsync: {0}",
|
||||||
|
"큰따옴표 (\") 가 든 인수는 Windows 의 rsync 에 그대로 넘길 수 없어 실행하지 않았습니다: {0}": "含有双引号 (\") 的参数无法原样传给 Windows 上的 rsync,因此未执行: {0}",
|
||||||
"키 있음": "已有密钥",
|
"키 있음": "已有密钥",
|
||||||
"타갈로그어": "他加禄语",
|
"타갈로그어": "他加禄语",
|
||||||
"타밀어": "泰米尔语",
|
"타밀어": "泰米尔语",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import '../platform/windows/rsync_installer.dart';
|
||||||
import 'app_controller.dart';
|
import 'app_controller.dart';
|
||||||
import 'settings.dart';
|
import 'settings.dart';
|
||||||
import '../core/rsync_filter.dart';
|
import '../core/rsync_filter.dart';
|
||||||
|
import '../core/rsync_launch.dart';
|
||||||
|
|
||||||
/// 앱에 들어 있는 rsync (없으면 null). 시작할 때 [findBundledRsync] 로 정한다.
|
/// 앱에 들어 있는 rsync (없으면 null). 시작할 때 [findBundledRsync] 로 정한다.
|
||||||
/// - Windows: 실행 파일 옆 rsync/rsync.exe (tool/fetch_rsync.ps1 로 준비해 빌드에 넣음)
|
/// - Windows: 실행 파일 옆 rsync/rsync.exe (tool/fetch_rsync.ps1 로 준비해 빌드에 넣음)
|
||||||
|
|
@ -630,7 +631,15 @@ class LiveSync extends ChangeNotifier {
|
||||||
if (exe == null) throw StateError(tr('rsync 가 없습니다 (환경 설정 > 파일 탐색기에서 내려받기 · 경로 지정)'));
|
if (exe == null) throw StateError(tr('rsync 가 없습니다 (환경 설정 > 파일 탐색기에서 내려받기 · 경로 지정)'));
|
||||||
final args = rsyncArgsFor(x, s, windows: Platform.isWindows);
|
final args = rsyncArgsFor(x, s, windows: Platform.isWindows);
|
||||||
await Directory(x.target).create(recursive: true);
|
await Directory(x.target).create(recursive: true);
|
||||||
final r = await Process.run(exe, args, stdoutEncoding: utf8, stderrEncoding: utf8);
|
// P0 (10/20): Windows 의 rsync 는 응답 파일 · MSYS=noglob 으로 (' · * 를 런타임이 다시 읽지 않게 - [prepareRsyncLaunch])
|
||||||
|
final launch = await prepareRsyncLaunch(args);
|
||||||
|
final ProcessResult r;
|
||||||
|
try {
|
||||||
|
r = await Process.run(exe, launch.args,
|
||||||
|
workingDirectory: launch.workingDirectory, environment: launch.environment, stdoutEncoding: utf8, stderrEncoding: utf8);
|
||||||
|
} finally {
|
||||||
|
launch.dispose();
|
||||||
|
}
|
||||||
if (!rsyncOk(r.exitCode)) throw ProcessException(exe, const [], '${r.stderr}'.trim(), r.exitCode);
|
if (!rsyncOk(r.exitCode)) throw ProcessException(exe, const [], '${r.stderr}'.trim(), r.exitCode);
|
||||||
return RsyncOutput().feed('${r.stdout}\n').length;
|
return RsyncOutput().feed('${r.stdout}\n').length;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import '../core/sync_tools.dart';
|
||||||
import '../core/vfs.dart';
|
import '../core/vfs.dart';
|
||||||
import '../l10n/tr.dart';
|
import '../l10n/tr.dart';
|
||||||
import '../core/rsync_filter.dart';
|
import '../core/rsync_filter.dart';
|
||||||
|
import '../core/rsync_launch.dart';
|
||||||
|
|
||||||
/// 복사 · 이동 한 번 (파일 탐색기). 방법 (현재 방식 · rsync · robocopy) 과 상관없이 같은 진행 상태를 알린다:
|
/// 복사 · 이동 한 번 (파일 탐색기). 방법 (현재 방식 · rsync · robocopy) 과 상관없이 같은 진행 상태를 알린다:
|
||||||
/// - 위쪽: 고른 항목 (폴더) 중 몇 번째인지 · 전체 진행률
|
/// - 위쪽: 고른 항목 (폴더) 중 몇 번째인지 · 전체 진행률
|
||||||
|
|
@ -401,8 +402,20 @@ class TransferJob extends ChangeNotifier {
|
||||||
if (log.length > 20) log.removeAt(0);
|
if (log.length > 20) log.removeAt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> _start(String exe, List<String> args, Encoding enc, void Function(String chunk) onOut) async {
|
/// [rsync]: Windows 의 rsync 는 응답 파일 · MSYS=noglob 으로 띄운다 (P0 10/20 - ' · * 를 런타임이 다시 읽지 않게, [prepareRsyncLaunch]).
|
||||||
_proc = await Process.start(exe, args, runInShell: false);
|
/// robocopy 는 Windows 프로그램이라 그대로
|
||||||
|
Future<int> _start(String exe, List<String> args, Encoding enc, void Function(String chunk) onOut, {bool rsync = false}) async {
|
||||||
|
final launch = rsync ? await prepareRsyncLaunch(args) : null;
|
||||||
|
try {
|
||||||
|
return await _startRaw(exe, launch?.args ?? args, enc, onOut, workingDirectory: launch?.workingDirectory, environment: launch?.environment);
|
||||||
|
} finally {
|
||||||
|
launch?.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> _startRaw(String exe, List<String> args, Encoding enc, void Function(String chunk) onOut,
|
||||||
|
{String? workingDirectory, Map<String, String>? environment}) async {
|
||||||
|
_proc = await Process.start(exe, args, runInShell: false, workingDirectory: workingDirectory, environment: environment);
|
||||||
final a = _proc!.stdout.transform(enc.decoder).listen(onOut);
|
final a = _proc!.stdout.transform(enc.decoder).listen(onOut);
|
||||||
final b = _proc!.stderr.transform(enc.decoder).listen((t) {
|
final b = _proc!.stderr.transform(enc.decoder).listen((t) {
|
||||||
for (final l in t.split(RegExp(r'[\r\n]'))) {
|
for (final l in t.split(RegExp(r'[\r\n]'))) {
|
||||||
|
|
@ -459,6 +472,7 @@ class TransferJob extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
_tick();
|
_tick();
|
||||||
},
|
},
|
||||||
|
rsync: true,
|
||||||
);
|
);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
if (!rsyncOk(code)) throw ProcessException(exe, const [], rsyncFailureText(code, errLog, log), code);
|
if (!rsyncOk(code)) throw ProcessException(exe, const [], rsyncFailureText(code, errLog, log), code);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import '../l10n/tr.dart';
|
||||||
|
|
||||||
|
/// 관리자 지시 (10/20, 28 재현 - P0 데이터): Windows 의 rsync (MSYS2 · Cygwin 런타임) 는 Windows 프로그램이 넘긴 명령 줄을
|
||||||
|
/// 스스로 다시 읽는다 - ' 를 따옴표로 짝지어 (--exclude=it's.txt --exclude=Bob's.txt 가 한 인수로 합쳐져 거르기가 사라지고
|
||||||
|
/// 지킬 파일이 지워짐, ' 가 든 경로는 합쳐져 실패) * ? [ ] { } 를 작업 폴더의 파일로 펼친다 (--exclude *.log 가 x.log y.log 로).
|
||||||
|
/// 앱이 만든 인수를 응답 파일 (@파일) 에 큰따옴표로 감싸 넣고 MSYS=noglob 으로 띄우면 그대로 간다 (실제 rsync 3.5.1 로 확인 -
|
||||||
|
/// MSYS=noglob 만으로는 ' 를 막지 못하고, CYGWIN=noglob 은 소용없다).
|
||||||
|
/// 응답 파일의 경로에 빈칸 · ' 가 있으면 @ 가 먹히지 않아, 그 파일이 든 폴더를 작업 폴더로 하고 "@args.txt" 로 넘긴다.
|
||||||
|
/// 큰따옴표 (") 가 든 인수는 이 방법으로도 그대로 넘기지 못한다 - 실행하지 않고 이유를 알린다 (Windows 파일 이름에는 " 가 없다).
|
||||||
|
class RsyncLaunch {
|
||||||
|
final List<String> args;
|
||||||
|
final String? workingDirectory;
|
||||||
|
final Map<String, String>? environment;
|
||||||
|
final Directory? _dir;
|
||||||
|
RsyncLaunch._(this.args, this.workingDirectory, this.environment, this._dir);
|
||||||
|
|
||||||
|
/// 다 쓴 응답 파일을 지운다 (rsync 가 끝난 뒤)
|
||||||
|
void dispose() {
|
||||||
|
try {
|
||||||
|
_dir?.deleteSync(recursive: true);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Windows rsync 에 그대로 넘길 수 없는 인수 (큰따옴표가 듦)
|
||||||
|
List<String> msysUnsafeArgs(List<String> args) => [for (final a in args) if (a.contains('"')) a];
|
||||||
|
|
||||||
|
/// [args] 를 rsync 에 그대로 넘기도록 준비한다 (Windows 만 응답 파일 · MSYS=noglob, 그 밖은 그대로). 끝나면 [RsyncLaunch.dispose]
|
||||||
|
Future<RsyncLaunch> prepareRsyncLaunch(List<String> args, {bool? windows}) async {
|
||||||
|
if (!(windows ?? Platform.isWindows)) return RsyncLaunch._(args, null, null, null);
|
||||||
|
final bad = msysUnsafeArgs(args);
|
||||||
|
if (bad.isNotEmpty) {
|
||||||
|
throw StateError(trf('큰따옴표 (") 가 든 인수는 Windows 의 rsync 에 그대로 넘길 수 없어 실행하지 않았습니다: {0}', [bad.join(' · ')]));
|
||||||
|
}
|
||||||
|
final dir = await Directory.systemTemp.createTemp('jj_rsargs_');
|
||||||
|
await File(p.join(dir.path, 'args.txt')).writeAsString(args.map((a) => '"$a"').join(' '), encoding: utf8);
|
||||||
|
final msys = Platform.environment['MSYS'];
|
||||||
|
return RsyncLaunch._(['@args.txt'], dir.path, {'MSYS': msys == null || msys.trim().isEmpty ? 'noglob' : '$msys noglob'}, dir);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
import 'file_ops.dart' show appTempRsyncExcludes;
|
import 'file_ops.dart' show appTempRsyncExcludes;
|
||||||
|
|
@ -195,13 +197,21 @@ const _rsyncLongWithValue = {
|
||||||
/// 관리자 지시 (10/20, 28 · 15 확인): [splitOptions] 로 고쳐 읽은 뒤에도 rsync 가 다르게 읽을 것 - 있으면 확인 창 · 실시간 동기화에
|
/// 관리자 지시 (10/20, 28 · 15 확인): [splitOptions] 로 고쳐 읽은 뒤에도 rsync 가 다르게 읽을 것 - 있으면 확인 창 · 실시간 동기화에
|
||||||
/// 빨갛게 알리고 실행을 막는다: 닫히지 않은 따옴표, 옵션 자리의 - 로 시작하지 않는 낱말 (rsync 는 원본 경로로 읽어 옵션이 몰래 빠짐),
|
/// 빨갛게 알리고 실행을 막는다: 닫히지 않은 따옴표, 옵션 자리의 - 로 시작하지 않는 낱말 (rsync 는 원본 경로로 읽어 옵션이 몰래 빠짐),
|
||||||
/// 하이픈 셋 이상, 끝에 값 없이 남은 값 받는 옵션. 옵션의 값 자리 (--exclude 한글.txt 의 한글.txt, -f 뒤) 는 세지 않는다
|
/// 하이픈 셋 이상, 끝에 값 없이 남은 값 받는 옵션. 옵션의 값 자리 (--exclude 한글.txt 의 한글.txt, -f 뒤) 는 세지 않는다
|
||||||
List<String> unreadableOptions(String options) {
|
List<String> unreadableOptions(String options, {bool? windows}) {
|
||||||
|
windows ??= Platform.isWindows;
|
||||||
final parsed = _parseOptions(options);
|
final parsed = _parseOptions(options);
|
||||||
// 15 확인 F1: 닫히지 않은 따옴표 - 뒤의 옵션이 다르게 읽힐 수 있다
|
// 15 확인 F1: 닫히지 않은 따옴표 - 뒤의 옵션이 다르게 읽힐 수 있다
|
||||||
final bad = [for (final u in parsed.unclosed) trf('따옴표가 닫히지 않았습니다: {0}', [u])];
|
final bad = [for (final u in parsed.unclosed) trf('따옴표가 닫히지 않았습니다: {0}', [u])];
|
||||||
var value = false;
|
var value = false;
|
||||||
var last = '';
|
var last = '';
|
||||||
for (final t in parsed.words) {
|
for (final t in parsed.words) {
|
||||||
|
// P0 (10/20): Windows 의 rsync (MSYS2) 에는 큰따옴표가 든 인수를 그대로 넘길 수 없다 (prepareRsyncLaunch) - 값 자리도.
|
||||||
|
// 닫히지 않은 따옴표로 이미 막혔으면 (그 따옴표가 낱말에 남음) 한 번만 알린다
|
||||||
|
if (windows && parsed.unclosed.isEmpty && t.contains('"')) {
|
||||||
|
bad.add(trf('큰따옴표 (") 가 든 옵션은 Windows 의 rsync 에 넘길 수 없습니다: {0}', [t]));
|
||||||
|
value = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (value) {
|
if (value) {
|
||||||
value = false;
|
value = false;
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
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/app/transfer_job.dart';
|
||||||
|
import 'package:jj_mkvmaker/core/rsync_launch.dart';
|
||||||
|
import 'package:jj_mkvmaker/core/sync_tools.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/rsync_scenes.dart' show sceneRsync, treeOf;
|
||||||
|
|
||||||
|
/// P0 (관리자 지시 10/20, 28 재현 - 데이터): Windows 의 rsync (MSYS2 런타임) 가 명령 줄을 다시 읽어 ' 를 짝짓고 (거르기가 사라져
|
||||||
|
/// 지킬 파일이 지워짐 · ' 경로가 합쳐져 실패) * ? [ ] { } 를 작업 폴더의 파일로 펼치던 것 - 응답 파일 + MSYS=noglob 으로 그대로 넘긴다.
|
||||||
|
/// 앱이 rsync 를 띄우는 두 길 (복사 · Rsync 화면 · 모니터링 · 최종 정리 = TransferJob, 실시간 동기화 = LiveSync) 을 실제 rsync 로.
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
test('도우미: Windows 는 응답 파일 · MSYS=noglob, 그 밖은 그대로 · 큰따옴표가 든 인수는 막음', () async {
|
||||||
|
final plain = await prepareRsyncLaunch(['-a', "it's"], windows: false);
|
||||||
|
expect(plain.args, ['-a', "it's"]);
|
||||||
|
expect(plain.environment, isNull);
|
||||||
|
final w = await prepareRsyncLaunch(['-a', "--exclude=it's.txt", 'a b'], windows: true);
|
||||||
|
addTearDown(w.dispose);
|
||||||
|
expect(w.args, ['@args.txt']);
|
||||||
|
expect(w.environment!['MSYS'], contains('noglob'));
|
||||||
|
expect(File(p.join(w.workingDirectory!, 'args.txt')).readAsStringSync(), '"-a" "--exclude=it\'s.txt" "a b"');
|
||||||
|
await expectLater(prepareRsyncLaunch(['--exclude=a"b'], windows: true), throwsA(isA<StateError>()));
|
||||||
|
// 옵션 칸에서도 미리 빨갛게 (Windows 만)
|
||||||
|
expect(unreadableOptions('-a --exclude=\'a"b\'', windows: true), hasLength(1));
|
||||||
|
expect(unreadableOptions('-a --exclude=\'a"b\'', windows: false), isEmpty);
|
||||||
|
expect(unreadableOptions('-a --exclude="it\'s.txt" --exclude="Bob\'s.txt"', windows: true), isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('실제 rsync', () {
|
||||||
|
final rsync = sceneRsync;
|
||||||
|
late Directory tmp;
|
||||||
|
setUp(() => tmp = Directory.systemTemp.createTempSync('jj_msysargs_'));
|
||||||
|
tearDown(() => tmp.deleteSync(recursive: true));
|
||||||
|
|
||||||
|
/// 원본 · 대상 폴더를 만들고 파일을 넣는다 (내용 = 쪽:이름)
|
||||||
|
(String, String) scene(String sn, String dn, List<String> sf, List<String> df) {
|
||||||
|
final s = Directory(p.join(tmp.path, sn))..createSync(recursive: true);
|
||||||
|
final d = Directory(p.join(tmp.path, dn))..createSync(recursive: true);
|
||||||
|
for (final f in sf) {
|
||||||
|
File(p.join(s.path, f))
|
||||||
|
..parent.createSync(recursive: true)
|
||||||
|
..writeAsStringSync('s:$f');
|
||||||
|
}
|
||||||
|
for (final f in df) {
|
||||||
|
File(p.join(d.path, f))
|
||||||
|
..parent.createSync(recursive: true)
|
||||||
|
..writeAsStringSync('d:$f');
|
||||||
|
}
|
||||||
|
return (s.path, d.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (이름, 옵션, 원본 폴더, 대상 폴더, 원본 파일, 대상 파일, 남아야 할 것 (대상), 없어야 할 것 (대상))
|
||||||
|
final cases = <(String, String, String, String, List<String>, List<String>, List<String>, List<String>)>[
|
||||||
|
("28 재현: ' 두 개의 --exclude", '-a --delete --exclude="it\'s.txt" --exclude="Bob\'s.txt"', 's', 'd', ['a.txt'],
|
||||||
|
["it's.txt", "Bob's.txt", 'old.txt'], ["it's.txt", "Bob's.txt", 'a.txt'], ['old.txt']),
|
||||||
|
("' 하나의 --exclude", "-a --delete --exclude=\"it's.txt\"", 's', 'd', ['a.txt'], ["it's.txt", 'old.txt'], ["it's.txt", 'a.txt'], ['old.txt']),
|
||||||
|
("' 가 든 원본 경로", '-a --delete', "s'1", 'd', ["a'b.txt"], ['old.txt'], ["a'b.txt"], ['old.txt']),
|
||||||
|
("' 가 든 원본 · 대상 경로", '-a --delete', "s'1", "d'2", ['a.txt', "c'd/e'f.txt"], ['old.txt'], ['a.txt', "c'd/e'f.txt"], ['old.txt']),
|
||||||
|
("' 경로 + ' --exclude", "-a --delete --exclude=\"it's.txt\"", "s'1", "d'2", ['a.txt'], ["it's.txt", 'old.txt'], ["it's.txt", 'a.txt'],
|
||||||
|
['old.txt']),
|
||||||
|
('따로 쓴 * (작업 폴더에 맞는 파일이 있어도)', '-a --delete --exclude *.txt', 's', 'd', ['a.txt', 'n.log'], ['keep.txt', 'old.log'],
|
||||||
|
['keep.txt', 'n.log'], ['old.log', 'a.txt']),
|
||||||
|
('따로 쓴 ? · [ ] · { }', '-a --delete --exclude b?.txt --exclude [x]* --exclude {q,r}.txt', 's', 'd', ['a.txt'],
|
||||||
|
['b9.txt', 'xk.txt', '{q,r}.txt', 'q.txt', 'old.txt'], ['b9.txt', 'xk.txt', '{q,r}.txt', 'a.txt'], ['q.txt', 'old.txt']),
|
||||||
|
('[ ] · { } · 빈칸 · 한글이 든 경로', '-a --delete', '원본 [1] {a}', "받는 쪽 [2] {b}'s", ['가 나.txt'], ['old.txt'], ['가 나.txt'], ['old.txt']),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final (name, opts, sn, dn, sf, df, keep, gone) in cases) {
|
||||||
|
test('복사 작업 (TransferJob): $name', () async {
|
||||||
|
if (rsync == null) return markTestSkipped('앱에 든 rsync 가 없음');
|
||||||
|
final (s, d) = scene(sn, dn, sf, df);
|
||||||
|
// 작업 폴더에 글로브에 맞을 파일을 둔다 (예전에는 따로 쓴 * 가 이것들로 펼쳐졌다)
|
||||||
|
final cwd = Directory.current;
|
||||||
|
final job = TransferJob(sources: [s], dest: d, move: false, method: CopyMethod.rsync, options: opts, rsyncExe: rsync, contents: true);
|
||||||
|
await job.run();
|
||||||
|
expect(job.error, isNull, reason: '${job.error}');
|
||||||
|
final t = treeOf(d);
|
||||||
|
for (final k in keep) {
|
||||||
|
expect(t.containsKey(k), isTrue, reason: '$name: $k 가 있어야 함 - $t');
|
||||||
|
}
|
||||||
|
for (final g in gone) {
|
||||||
|
expect(t.containsKey(g), isFalse, reason: '$name: $g 는 없어야 함 - $t');
|
||||||
|
}
|
||||||
|
expect(Directory.current.path, cwd.path);
|
||||||
|
}, skip: Platform.isWindows ? false : 'Windows 만 (MSYS2 rsync)');
|
||||||
|
}
|
||||||
|
|
||||||
|
test("실시간 동기화 (LiveSync rsync 쌍): ' 두 개의 --exclude · ' 가 든 경로", () async {
|
||||||
|
if (rsync == null) return markTestSkipped('앱에 든 rsync 가 없음');
|
||||||
|
final (s, d) = scene("s'1", "d'2", ['a.txt'], ["it's.txt", "Bob's.txt", 'old.txt']);
|
||||||
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||||
|
c.settings
|
||||||
|
..rsyncSource = 'custom'
|
||||||
|
..rsyncPath = rsync
|
||||||
|
..rsyncOptions = '-a --exclude="it\'s.txt" --exclude="Bob\'s.txt"'
|
||||||
|
..liveSyncPairs = [LiveSyncPair(s, d, method: 'rsync', delete: true, deleteConfirmed: true, confirmedOptions: '-a --exclude="it\'s.txt" --exclude="Bob\'s.txt"')];
|
||||||
|
final live = LiveSync(c);
|
||||||
|
addTearDown(live.dispose);
|
||||||
|
final pair = c.settings.liveSyncPairs.single;
|
||||||
|
await live.syncNow(pair);
|
||||||
|
expect(live.status[LiveSync.keyOf(pair)]!.$2, isNot(contains('실패')));
|
||||||
|
final t = treeOf(d);
|
||||||
|
expect(t.keys.toSet(), {"it's.txt", "Bob's.txt", 'a.txt'}, reason: '거르기를 지키고 old.txt 만 지움');
|
||||||
|
}, skip: Platform.isWindows ? false : 'Windows 만 (MSYS2 rsync)');
|
||||||
|
|
||||||
|
test('응답 파일은 끝나면 지운다', () async {
|
||||||
|
if (rsync == null) return markTestSkipped('앱에 든 rsync 가 없음');
|
||||||
|
final (s, d) = scene('s', 'd', ['a.txt'], []);
|
||||||
|
final job = TransferJob(sources: [s], dest: d, move: false, method: CopyMethod.rsync, options: '-a', rsyncExe: rsync, contents: true);
|
||||||
|
await job.run();
|
||||||
|
expect(job.error, isNull);
|
||||||
|
// 이 작업의 응답 파일 (이 시험의 경로가 든 것) 이 남지 않았는지 - 함께 도는 다른 시험의 것은 세지 않는다
|
||||||
|
final mine = [
|
||||||
|
for (final e in Directory.systemTemp.listSync())
|
||||||
|
if (p.basename(e.path).startsWith('jj_rsargs_') && File(p.join(e.path, 'args.txt')).existsSync())
|
||||||
|
if (File(p.join(e.path, 'args.txt')).readAsStringSync().contains(toCygwinPath(s))) e.path,
|
||||||
|
];
|
||||||
|
expect(mine, isEmpty);
|
||||||
|
}, skip: Platform.isWindows ? false : 'Windows 만 (MSYS2 rsync)');
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue