원본 지우기 옵션: 확인 창에 보이는 것과 실제 실행을 같게 (178, 데이터)

환경 설정의 rsync 옵션에 --remove-source-files 가 있으면 [좌 → 우] 확인 창은 [원본 파일 지우기] 꺼짐 · [실행] 인데
실행하면 원본이 지워졌다 (사용자는 복사라고 믿음).
- rsyncArgs · robocopyRuns: 원본을 지우는 옵션 (--remove-source-files · --remove-sent-files · /MOV · /MOVE) 은 옵션 글에서 늘 빼고
  move (이동 · [원본 파일 지우기]) 일 때만 넣는다 - 탐색기 복사 · Rsync 확인 창 · 모니터링 다시 실행 모두.
- Rsync 확인 창: 설정 (기억한) 옵션에 원본 지우기가 있으면 체크를 켠 채로 · [이동] · 경고. 끄면 명령 줄 · 실행에서 빠진다.
  ⇄ 는 원본을 지우지 않는다고 알린다. 복사 확인 창 · 모니터링 카드의 옵션 줄도 실제 쓰는 것으로.
- sync_tools_test · explorer_webdav_test (끄면 원본이 남음).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음4
RyuJongJin 2026-10-10 06:08:47 +09:00
parent d5bb200d00
commit ae51fdfa34
9 changed files with 97 additions and 16 deletions

View File

@ -1209,6 +1209,7 @@
"약 {0} 남음": "about {0} left", "약 {0} 남음": "about {0} left",
"약 {0}~{1}분": "about {0}–{1} min", "약 {0}~{1}분": "about {0}–{1} min",
"약 {0}~{1}분 (동영상 {2})": "about {0}–{1} min (videos: {2})", "약 {0}~{1}분 (동영상 {2})": "about {0}–{1} min (videos: {2})",
"양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).": "Both ways (⇄) never deletes source files (--remove-source-files in the options is not used).",
"양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "Both ways: newer files on the receiving side are not overwritten (-u).", "양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "Both ways: newer files on the receiving side are not overwritten (-u).",
"어둡게": "Darker", "어둡게": "Darker",
"어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "Press anywhere to bring up the JJ_MKVMaker window", "어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "Press anywhere to bring up the JJ_MKVMaker window",
@ -1314,6 +1315,8 @@
"원본 유지 (재인코딩 없음)": "Keep original (no re-encoding)", "원본 유지 (재인코딩 없음)": "Keep original (no re-encoding)",
"원본 크기": "Original size", "원본 크기": "Original size",
"원본 파일 지우기 (--remove-source-files)": "Delete source files (--remove-source-files)", "원본 파일 지우기 (--remove-source-files)": "Delete source files (--remove-source-files)",
"원본 파일이 지워집니다 (이동).": "Source files will be deleted (move).",
"원본 파일이 지워집니다 (이동). 환경 설정의 rsync 옵션에 --remove-source-files 가 있어 켜 두었습니다 - 끄면 이번에는 원본을 남깁니다.": "Source files will be deleted (move). This is on because the rsync options in Settings contain --remove-source-files - turn it off to keep the source this time.",
"원본 폴더": "Source folder", "원본 폴더": "Source folder",
"원본 폴더가 바뀌면 곧바로 대상 폴더에 맞춥니다 (앱이 켜져 있는 동안). 시작할 때 한 번 맞춥니다.": "When the source folder changes, the target is synced right away (while the app is running). Also syncs once at start.", "원본 폴더가 바뀌면 곧바로 대상 폴더에 맞춥니다 (앱이 켜져 있는 동안). 시작할 때 한 번 맞춥니다.": "When the source folder changes, the target is synced right away (while the app is running). Also syncs once at start.",
"원본 폴더가 비어 있어 멈춤": "Stopped: source folder is empty", "원본 폴더가 비어 있어 멈춤": "Stopped: source folder is empty",

View File

@ -1209,6 +1209,7 @@
"약 {0} 남음": "残り約 {0}", "약 {0} 남음": "残り約 {0}",
"약 {0}~{1}분": "約 {0}~{1} 分", "약 {0}~{1}분": "約 {0}~{1} 分",
"약 {0}~{1}분 (동영상 {2})": "約 {0}~{1} 分 (動画 {2})", "약 {0}~{1}분 (동영상 {2})": "約 {0}~{1} 分 (動画 {2})",
"양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).": "双方向 (⇄) では元のファイルを削除しません (オプションの --remove-source-files は使いません)。",
"양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "双方向: 受け取る側の方が新しいファイルは上書きしません (-u)。", "양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "双方向: 受け取る側の方が新しいファイルは上書きしません (-u)。",
"어둡게": "暗く", "어둡게": "暗く",
"어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "どこからでも押すと JJ_MKVMaker ウィンドウが表示されます", "어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "どこからでも押すと JJ_MKVMaker ウィンドウが表示されます",
@ -1314,6 +1315,8 @@
"원본 유지 (재인코딩 없음)": "元のまま(再エンコードなし)", "원본 유지 (재인코딩 없음)": "元のまま(再エンコードなし)",
"원본 크기": "元のサイズ", "원본 크기": "元のサイズ",
"원본 파일 지우기 (--remove-source-files)": "元ファイルを削除 (--remove-source-files)", "원본 파일 지우기 (--remove-source-files)": "元ファイルを削除 (--remove-source-files)",
"원본 파일이 지워집니다 (이동).": "元のファイルは削除されます (移動)。",
"원본 파일이 지워집니다 (이동). 환경 설정의 rsync 옵션에 --remove-source-files 가 있어 켜 두었습니다 - 끄면 이번에는 원본을 남깁니다.": "元のファイルは削除されます (移動)。設定の rsync オプションに --remove-source-files があるためオンにしています - オフにすると今回は元を残します。",
"원본 폴더": "元フォルダー", "원본 폴더": "元フォルダー",
"원본 폴더가 바뀌면 곧바로 대상 폴더에 맞춥니다 (앱이 켜져 있는 동안). 시작할 때 한 번 맞춥니다.": "元フォルダーが変わるとすぐ対象フォルダーに合わせます (アプリの起動中)。起動時にも 1 回合わせます。", "원본 폴더가 바뀌면 곧바로 대상 폴더에 맞춥니다 (앱이 켜져 있는 동안). 시작할 때 한 번 맞춥니다.": "元フォルダーが変わるとすぐ対象フォルダーに合わせます (アプリの起動中)。起動時にも 1 回合わせます。",
"원본 폴더가 비어 있어 멈춤": "元フォルダーが空のため停止", "원본 폴더가 비어 있어 멈춤": "元フォルダーが空のため停止",

View File

@ -1209,6 +1209,7 @@
"약 {0} 남음": "约剩 {0}", "약 {0} 남음": "约剩 {0}",
"약 {0}~{1}분": "约 {0}~{1} 分钟", "약 {0}~{1}분": "约 {0}~{1} 分钟",
"약 {0}~{1}분 (동영상 {2})": "约 {0}~{1} 分钟(视频 {2})", "약 {0}~{1}분 (동영상 {2})": "约 {0}~{1} 分钟(视频 {2})",
"양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).": "双向 (⇄) 不会删除源文件(不使用选项中的 --remove-source-files)。",
"양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "双向: 接收端较新的文件不会被覆盖 (-u)。", "양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "双向: 接收端较新的文件不会被覆盖 (-u)。",
"어둡게": "调暗", "어둡게": "调暗",
"어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "在任何地方按下即可显示 JJ_MKVMaker 窗口", "어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "在任何地方按下即可显示 JJ_MKVMaker 窗口",
@ -1314,6 +1315,8 @@
"원본 유지 (재인코딩 없음)": "保持原样(不重新编码)", "원본 유지 (재인코딩 없음)": "保持原样(不重新编码)",
"원본 크기": "原始尺寸", "원본 크기": "原始尺寸",
"원본 파일 지우기 (--remove-source-files)": "删除源文件 (--remove-source-files)", "원본 파일 지우기 (--remove-source-files)": "删除源文件 (--remove-source-files)",
"원본 파일이 지워집니다 (이동).": "源文件将被删除(移动)。",
"원본 파일이 지워집니다 (이동). 환경 설정의 rsync 옵션에 --remove-source-files 가 있어 켜 두었습니다 - 끄면 이번에는 원본을 남깁니다.": "源文件将被删除(移动)。因为设置中的 rsync 选项含有 --remove-source-files 所以已勾选 - 取消勾选则本次保留源文件。",
"원본 폴더": "源文件夹", "원본 폴더": "源文件夹",
"원본 폴더가 바뀌면 곧바로 대상 폴더에 맞춥니다 (앱이 켜져 있는 동안). 시작할 때 한 번 맞춥니다.": "源文件夹变化时立即同步到目标 (应用运行期间)。启动时也同步一次。", "원본 폴더가 바뀌면 곧바로 대상 폴더에 맞춥니다 (앱이 켜져 있는 동안). 시작할 때 한 번 맞춥니다.": "源文件夹变化时立即同步到目标 (应用运行期间)。启动时也同步一次。",
"원본 폴더가 비어 있어 멈춤": "源文件夹为空,已停止", "원본 폴더가 비어 있어 멈춤": "源文件夹为空,已停止",

View File

@ -1,5 +1,7 @@
import 'file_ops.dart' show isPartialFile, SourceUnreadableException; import 'file_ops.dart' show isPartialFile, SourceUnreadableException;
import 'sync_tools.dart' show splitOptions; import 'sync_tools.dart' show splitOptions, isSourceRemovingOption;
export 'sync_tools.dart' show isSourceRemovingOption;
import 'vfs.dart'; import 'vfs.dart';
/// rsync (Rsync 화면의 → · ← · ⇄) 를 실행하기 전에 무엇이 바뀌는지 미리 보기 (72). /// rsync (Rsync 화면의 → · ← · ⇄) 를 실행하기 전에 무엇이 바뀌는지 미리 보기 (72).
@ -64,9 +66,13 @@ String withoutDeleteOptions(String options) => rawOptionTokens(options)
.where((raw) => !splitOptions(raw).any(isRsyncDeleteOption)) .where((raw) => !splitOptions(raw).any(isRsyncDeleteOption))
.join(' '); .join(' ');
/// 원본을 지우는 rsync · robocopy 옵션인지 (--remove-source-files · --remove-sent-files · /MOV · /MOVE) - 107 /// 178: 옵션 글에서 원본을 지우는 옵션을 뺀다 (원본 지우기는 [원본 파일 지우기] · [이동] 으로만 - 보이는 것과 실행이 같게)
bool isSourceRemovingOption(String o) => String withoutSourceRemovingOptions(String options) => rawOptionTokens(options)
o == '--remove-source-files' || o == '--remove-sent-files' || o.toUpperCase() == '/MOV' || o.toUpperCase() == '/MOVE'; .where((raw) => !splitOptions(raw).any(isSourceRemovingOption))
.join(' ');
/// 옵션 글에 원본을 지우는 옵션이 들어 있는지 (환경 설정의 rsync 옵션 등)
bool optionsRemoveSource(String options) => splitOptions(options).any(isSourceRemovingOption);
/// rsync 옵션에 -u (받는 쪽이 더 새 파일은 건너뜀) 가 있는지 /// rsync 옵션에 -u (받는 쪽이 더 새 파일은 건너뜀) 가 있는지
bool optionsUpdate(String options) => bool optionsUpdate(String options) =>

View File

@ -75,13 +75,14 @@ List<String> rsyncArgs({
bool windows = true, bool windows = true,
bool contents = false, bool contents = false,
}) { }) {
final opts = splitOptions(options); // 178: 원본을 지우는 옵션은 [move] (이동 · [원본 파일 지우기]) 로만 - 옵션 글에 있어도 빼고, 이동일 때만 넣는다
final opts = [for (final o in splitOptions(options)) if (!isSourceRemovingOption(o)) o];
return [ return [
...opts, ...opts,
if (bandwidthKBps > 0 && !opts.any((o) => o.startsWith('--bwlimit'))) '--bwlimit=$bandwidthKBps', if (bandwidthKBps > 0 && !opts.any((o) => o.startsWith('--bwlimit'))) '--bwlimit=$bandwidthKBps',
// 한글 등 이름을 \#355… 로 바꾸지 않고 그대로 출력 (Android 빌드는 iconv 없음 · 진행 표시에 쓰는 이름) // 한글 등 이름을 \#355… 로 바꾸지 않고 그대로 출력 (Android 빌드는 iconv 없음 · 진행 표시에 쓰는 이름)
if (!windows && !opts.contains('-8') && !opts.contains('--8-bit-output')) '-8', if (!windows && !opts.contains('-8') && !opts.contains('--8-bit-output')) '-8',
if (move && !opts.contains('--remove-source-files')) '--remove-source-files', if (move) '--remove-source-files',
// [contents]: 폴더 "안의 것" 을 대상에 (rsync 의 끝 / - 동기화 · lsync 와 같은 모양) // [contents]: 폴더 "안의 것" 을 대상에 (rsync 의 끝 / - 동기화 · lsync 와 같은 모양)
for (final s in sources) '${toCygwinPath(_noTrailingSlash(s), windows: windows)}${contents ? '/' : ''}', for (final s in sources) '${toCygwinPath(_noTrailingSlash(s), windows: windows)}${contents ? '/' : ''}',
'${toCygwinPath(_noTrailingSlash(dest), windows: windows)}/', '${toCygwinPath(_noTrailingSlash(dest), windows: windows)}/',
@ -90,6 +91,10 @@ List<String> rsyncArgs({
String _noTrailingSlash(String s) => s.length > 3 && (s.endsWith('/') || s.endsWith('\\')) ? s.substring(0, s.length - 1) : s; String _noTrailingSlash(String s) => s.length > 3 && (s.endsWith('/') || s.endsWith('\\')) ? s.substring(0, s.length - 1) : s;
/// 원본을 지우는 rsync · robocopy 옵션인지 (--remove-source-files · --remove-sent-files · /MOV · /MOVE) - 107 · 178
bool isSourceRemovingOption(String o) =>
o == '--remove-source-files' || o == '--remove-sent-files' || o.toUpperCase() == '/MOV' || o.toUpperCase() == '/MOVE';
/// robocopy 한 번 실행할 인수 (robocopy 는 "폴더 → 폴더" 라 원본마다 따로 만든다). /// robocopy 한 번 실행할 인수 (robocopy 는 "폴더 → 폴더" 라 원본마다 따로 만든다).
/// - 폴더: robocopy <폴더> <대상>\<폴더 이름> [옵션] /// - 폴더: robocopy <폴더> <대상>\<폴더 이름> [옵션]
/// - 파일: robocopy <파일이 있는 폴더> <대상> <파일 이름들…> [옵션] (같은 폴더의 파일은 한 번에) /// - 파일: robocopy <파일이 있는 폴더> <대상> <파일 이름들…> [옵션] (같은 폴더의 파일은 한 번에)
@ -103,10 +108,11 @@ List<List<String>> robocopyRuns({
bool move = false, bool move = false,
bool contents = false, bool contents = false,
}) { }) {
final opts = splitOptions(options); // 178: 원본을 지우는 옵션 (/MOV · /MOVE) 은 [move] 로만
final opts = [for (final o in splitOptions(options)) if (!isSourceRemovingOption(o)) o];
final extra = [ final extra = [
if (bandwidthKBps > 0 && !opts.any((o) => o.toUpperCase().startsWith('/IPG'))) '/IPG:${robocopyIpg(bandwidthKBps)}', if (bandwidthKBps > 0 && !opts.any((o) => o.toUpperCase().startsWith('/IPG'))) '/IPG:${robocopyIpg(bandwidthKBps)}',
if (move && !opts.any((o) => o.toUpperCase().startsWith('/MOV'))) '/MOVE', if (move) '/MOVE',
]; ];
final runs = <List<String>>[ final runs = <List<String>>[
for (final f in folders) [f, contents ? dest : p.join(dest, p.basename(f)), ...opts, ...extra], for (final f in folders) [f, contents ? dest : p.join(dest, p.basename(f)), ...opts, ...extra],

View File

@ -1736,7 +1736,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
'\n${_namesOf(sources)}' '\n${_namesOf(sources)}'
'${skippedHere.isEmpty ? '' : '\n${trf('이미 이 폴더에 있어 건너뜀: {0}', [_namesOf(skippedHere)])}'}' '${skippedHere.isEmpty ? '' : '\n${trf('이미 이 폴더에 있어 건너뜀: {0}', [_namesOf(skippedHere)])}'}'
'\n\n${trf('방법: {0}', [tr(method.label)])}' '\n\n${trf('방법: {0}', [tr(method.label)])}'
'${method == CopyMethod.builtin ? '' : ' · ${task.options}'}' '${method == CopyMethod.builtin ? '' : ' · ${withoutSourceRemovingOptions(task.options)}${move ? (method == CopyMethod.rsync ? ' --remove-source-files' : ' /MOVE') : ''}'}'
'${task.bandwidthKBps > 0 ? ' · ${trf('속도 제한 {0} KB/s', [task.bandwidthKBps])}' : ''}'), '${task.bandwidthKBps > 0 ? ' · ${trf('속도 제한 {0} KB/s', [task.bandwidthKBps])}' : ''}'),
FutureBuilder<List<NameClash>>( FutureBuilder<List<NameClash>>(
future: clashesFuture, future: clashesFuture,
@ -1892,8 +1892,14 @@ class _ExplorerPageState extends State<ExplorerPage> {
// 96: 확인 창 · 미리 보기 · 실행이 같은 작업 (원본 파일 지우기 = 이동 작업) 과 같은 옵션을 쓴다 // 96: 확인 창 · 미리 보기 · 실행이 같은 작업 (원본 파일 지우기 = 이동 작업) 과 같은 옵션을 쓴다
List<CopyTask> shownTasks(bool move) => List<CopyTask> shownTasks(bool move) =>
[for (final (src, dst) in runs) center.peek([src], dst, contents: true, method: 'rsync', move: move)]; [for (final (src, dst) in runs) center.peek([src], dst, contents: true, method: 'rsync', move: move)];
var tasks = shownTasks(false); // 178: 환경 설정 (또는 기억한) 옵션에 원본 지우기가 있으면 [원본 파일 지우기] 를 켠 채로 연다 (버튼은 [이동] · 경고).
String opts(CopyTask t) => both ? withUpdateOption(t.options) : t.options; // 원본 지우기는 이 체크로만 정한다 - 옵션 글에서는 빼고 보이고, 끄면 이번 실행에서 정말 빠진다
final optionMove = runs.map((x) => center.peek([x.$1], x.$2, contents: true, method: 'rsync', move: false)).any((t) => optionsRemoveSource(t.options));
var tasks = shownTasks(optionMove && !both);
String opts(CopyTask t) {
final o = withoutSourceRemovingOptions(t.options);
return both ? withUpdateOption(o) : o;
}
// 95: ⇄ 는 → 다음에 ← (차례로). ← 에서는 지우지 않는다 (→ 가 방금 복사한 것을 지우지 않게) // 95: ⇄ 는 → 다음에 ← (차례로). ← 에서는 지우지 않는다 (→ 가 방금 복사한 것을 지우지 않게)
String runOpts(int i) => both && i > 0 ? withoutDeleteOptions(opts(tasks[i])) : opts(tasks[i]); String runOpts(int i) => both && i > 0 ? withoutDeleteOptions(opts(tasks[i])) : opts(tasks[i]);
if (nested && tasks.any((t) => optionsDelete(opts(t)))) { if (nested && tasks.any((t) => optionsDelete(opts(t)))) {
@ -1901,7 +1907,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
return; return;
} }
// 한 방향 (→ · ←) 만: 원본 파일 지우기 (--remove-source-files) 와 끝난 뒤 원본 정리 // 한 방향 (→ · ←) 만: 원본 파일 지우기 (--remove-source-files) 와 끝난 뒤 원본 정리
var removeSource = false; var removeSource = optionMove && !both;
var prune = 'keep'; var prune = 'keep';
// 72: 실행 전 비교 결과 (지워질 수를 실행 버튼에) // 72: 실행 전 비교 결과 (지워질 수를 실행 버튼에)
List<PreviewItem>? preview; List<PreviewItem>? preview;
@ -1986,10 +1992,13 @@ class _ExplorerPageState extends State<ExplorerPage> {
if (_viaDav([l, r])) if (_viaDav([l, r]))
Text(tr('WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).'), Text(tr('WebDAV: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files 를 따름).'),
style: const TextStyle(fontSize: 12, color: JjColors.accent)), style: const TextStyle(fontSize: 12, color: JjColors.accent)),
if (both) if (both) ...[
Text(tr('양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).'), Text(tr('양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)) style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
else ...[ if (optionMove)
Text(tr('양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)),
] else ...[
CheckboxListTile( CheckboxListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
value: removeSource, value: removeSource,
@ -2003,6 +2012,13 @@ class _ExplorerPageState extends State<ExplorerPage> {
subtitle: Text(tr('대상으로 옮긴 파일을 원본에서 지웁니다 (이동)')), subtitle: Text(tr('대상으로 옮긴 파일을 원본에서 지웁니다 (이동)')),
), ),
if (removeSource) ...[ if (removeSource) ...[
// 178: 원본이 지워진다는 것을 분명히 (환경 설정 옵션 때문에 켜졌으면 그렇다고)
Text(
optionMove
? tr('원본 파일이 지워집니다 (이동). 환경 설정의 rsync 옵션에 --remove-source-files 가 있어 켜 두었습니다 - 끄면 이번에는 원본을 남깁니다.')
: tr('원본 파일이 지워집니다 (이동).'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent, fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Text(tr('끝난 뒤 원본의 빈 폴더를 지웁니다 (find 원본/ -type d -empty -delete). 원본 폴더는:'), Text(tr('끝난 뒤 원본의 빈 폴더를 지웁니다 (find 원본/ -type d -empty -delete). 원본 폴더는:'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 6), const SizedBox(height: 6),

View File

@ -19,6 +19,7 @@ import 'copy_sync_settings.dart' show addLiveSyncPairDialog, editLiveSyncPairDia
import 'path_label.dart'; import 'path_label.dart';
import 'rsync_setup.dart'; import 'rsync_setup.dart';
import 'schedule_editor.dart'; import 'schedule_editor.dart';
import '../core/sync_preview.dart' show withoutSourceRemovingOptions;
import 'sync_preview_view.dart' show confirmDeletingRun; import 'sync_preview_view.dart' show confirmDeletingRun;
import 'theme.dart'; import 'theme.dart';
@ -373,7 +374,8 @@ class _CopyCardState extends State<_CopyCard> {
title: Text(tr('고친 옵션을 저장하지 않았습니다')), title: Text(tr('고친 옵션을 저장하지 않았습니다')),
content: Text(trf('칸에 보이는 옵션: {0}\n저장된 옵션: {1}\n\n고친 옵션을 저장하고 실행할까요?', [ content: Text(trf('칸에 보이는 옵션: {0}\n저장된 옵션: {1}\n\n고친 옵션을 저장하고 실행할까요?', [
'${_method == 'builtin' ? tr('현재 방식') : _method} ${_options.text.trim()}', '${_method == 'builtin' ? tr('현재 방식') : _method} ${_options.text.trim()}',
'${t.method == 'builtin' ? tr('현재 방식') : t.method} ${t.options}', // 178: 실제로 쓰는 옵션 (원본 지우기는 이동일 때만)
'${t.method == 'builtin' ? tr('현재 방식') : t.method} ${withoutSourceRemovingOptions(t.options)}${t.move ? (t.method == 'robocopy' ? ' /MOVE' : t.method == 'rsync' ? ' --remove-source-files' : '') : ''}',
])), ])),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('취소'))), TextButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('취소'))),

View File

@ -114,6 +114,39 @@ void main() {
expect(up.readAsStringSync(), 'hello'); expect(up.readAsStringSync(), 'hello');
}); });
testWidgets('178: 설정 옵션에 --remove-source-files 가 있으면 체크 켬 · [이동] · 경고, 끄면 정말 원본을 남긴다', (tester) async {
final s = DavServer(id: 'nas', name: '집 NAS', url: server.url, user: 'user', password: 'pass');
c.settings
..webdavServers = [s]
..rsyncOptions = '-av --remove-source-files'
..rsyncPaths = [left, 'dav://nas/'];
DavRegistry.configure([s]);
await pump(tester, ExplorerPage(c: c, rsync: true));
await settle(tester, () => find.text('sub').evaluate().isNotEmpty && find.text('원격').evaluate().isNotEmpty);
await act(tester, () => tester.tap(find.text('sub')));
await act(tester, () => tester.tap(find.text('원격')));
await settle(tester, () => false, rounds: 5);
await act(tester, () => tester.tap(find.text('좌 → 우')));
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 400);
// 보이는 것 = 실행될 것: 체크 켬 · [이동] · 경고 · 명령 줄에 한 번
CheckboxListTile box() => tester.widget<CheckboxListTile>(find.byType(CheckboxListTile));
expect(box().value, isTrue);
expect(find.widgetWithText(FilledButton, '이동'), findsOneWidget);
expect(find.textContaining('원본 파일이 지워집니다 (이동). 환경 설정의 rsync 옵션에'), findsOneWidget);
expect(find.text('rsync -av --remove-source-files'), findsOneWidget);
// 끄면 [실행] · 옵션 줄에서도 빠진다
await tester.tap(find.byType(CheckboxListTile));
await settle(tester, () => find.textContaining('비교하는 중').evaluate().isEmpty, rounds: 400);
expect(box().value, isFalse);
expect(find.widgetWithText(FilledButton, '실행'), findsOneWidget);
expect(find.text('rsync -av'), findsOneWidget);
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '실행')));
final got = File(p.join(remote.path, '원격', 'inner.txt'));
await settle(tester, () => got.existsSync() && find.byType(LinearProgressIndicator).evaluate().isEmpty, rounds: 200);
expect(got.readAsStringSync(), 'inner');
expect(File(p.join(left, 'sub', 'inner.txt')).existsSync(), isTrue, reason: '체크를 끄면 원본은 그대로 (복사)');
});
testWidgets('Rsync 화면: 로컬 ↔ WebDAV 폴더 → (앱이 직접 맞춤 · rsync 필요 없음)', (tester) async { testWidgets('Rsync 화면: 로컬 ↔ WebDAV 폴더 → (앱이 직접 맞춤 · rsync 필요 없음)', (tester) async {
final s = DavServer(id: 'nas', name: '집 NAS', url: server.url, user: 'user', password: 'pass'); final s = DavServer(id: 'nas', name: '집 NAS', url: server.url, user: 'user', password: 'pass');
c.settings c.settings

View File

@ -98,6 +98,15 @@ void main() {
expect(o.feed('100% \n\t Newer \t\t 200\tC:\\t\\src\\b.txt\n\t*EXTRA File \t\t 1\tC:\\x\n'), [r'C:\t\src\b.txt']); expect(o.feed('100% \n\t Newer \t\t 200\tC:\\t\\src\\b.txt\n\t*EXTRA File \t\t 1\tC:\\x\n'), [r'C:\t\src\b.txt']);
}); });
test('178: 원본을 지우는 옵션은 이동일 때만 (옵션 글에 있어도)', () {
List<String> r(bool move) => rsyncArgs(options: '-av --remove-source-files --remove-sent-files', sources: [r'C:\a'], dest: r'D:\b', move: move);
expect(r(false).where(isSourceRemovingOption), isEmpty);
expect(r(true).where(isSourceRemovingOption), ['--remove-source-files']);
List<String> rc(bool move) => robocopyRuns(options: '/E /MOV /MOVE', folders: [r'C:\a'], files: const [], dest: r'D:\b', move: move).single;
expect(rc(false).where(isSourceRemovingOption), isEmpty);
expect(rc(true).where(isSourceRemovingOption), ['/MOVE']);
});
test('61: robocopy 파일 줄은 Windows 언어와 상관없이 (일본어 · 중국어 · 한국어 종류 글)', () { test('61: robocopy 파일 줄은 Windows 언어와 상관없이 (일본어 · 중국어 · 한국어 종류 글)', () {
final o = RobocopyOutput(); final o = RobocopyOutput();
expect(o.feed('\t 新しいファイル \t\t 3000000\tC:\\t\\a b.txt\r\n 34% \r\n'), [r'C:\t\a b.txt']); expect(o.feed('\t 新しいファイル \t\t 3000000\tC:\\t\\a b.txt\r\n 34% \r\n'), [r'C:\t\a b.txt']);