WIP P0 원격 맞추기: --delete 가 없으면 받는 쪽의 같은 이름 폴더를 지우지 않음 (예전: 통째로 지우고 파일로) · 미리 보기에 보임 (검토 대기)
관리자 지시 (10/19, 15 의 원격 동기화 검토 M1 - P0): WebDAV · SFTP · SMB · FTP 가 끼면 앱이 직접 맞추는데 (file_ops._mirrorV), 원본은 파일 X, 받는 쪽에는 같은 이름의 **폴더** X 가 있으면 --delete 가 없어도 그 폴더를 통째로 지우고 파일로 바꿨다. 미리 보기에는 "바뀜" 한 줄로만 보여 사용자가 모르게 지워졌다 (10/8 WebDAV 부터 정식 판에 있던 것으로 봄). 실제 rsync 는 --delete (또는 --force) 없이는 비어 있지 않은 폴더를 지우지 않는다. - _mirrorV: --delete 가 없으면 그 이름은 건너뛰고 FileOps.dirConflicts 에 남긴다. --delete 면 지금처럼 지우고 파일로. - 미리 보기 (sync_preview): 지우기 없음 → "받는 쪽에 같은 이름의 폴더가 있어 건너뜀 - 폴더는 그대로" (주황, 새 PreviewAction.skipDir), 지우기 → 지울 목록에 "폴더 · 안의 n개를 지우고 같은 이름의 파일로" (빨강, 지울 수에 들어가 72 의 보호를 받음). - 알림: 복사 · 이동 작업 끝 "같은 이름의 폴더가 있어 건너뜀 n개: …", 실시간 동기화 (lsync, 원격 쌍) 카드 "n개 맞춤 · 같은 이름의 폴더가 있어 건너뜀 n개" (같은 규칙). - 로컬끼리 맞추기는 원래 폴더를 지우지 않았다 (파일 이름 바꾸기가 실패) - 그대로. 시험: webdav_test · sftp_test (새 각 1, 시험 WebDAV 서버 · paramiko SFTP 서버): 지우기 없음 → 미리 보기 skipDir · 지울 것 없음 · 실제로 폴더와 안의 파일 그대로 · 다른 파일은 맞춤 · dirConflicts 1 / 지우기 → 미리 보기 지우기 줄 (안의 n개) · 실제로 파일로. analyze 문제 없음, l10n 빠짐 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>next2
parent
47165371eb
commit
62784039e4
|
|
@ -59,11 +59,13 @@
|
|||
"(덮어씀)": "(overwritten)",
|
||||
"(동영상을 세 번 누르면 옮깁니다)": "(Tapping a video three times moves it)",
|
||||
"(링크 · junction - 따라가지 않고 건너뜀)": "(link · junction - not followed, skipped)",
|
||||
"(받는 쪽에 같은 이름의 폴더가 있어 건너뜀 - 폴더는 그대로)": "(skipped - the destination has a folder with the same name; the folder is kept)",
|
||||
"(받는 쪽에만 있음)": "(only on receiving side)",
|
||||
"(받는 쪽이 더 새것 - 건너뜀)": "(receiving side is newer - skipped)",
|
||||
"(비어 있음)": "(empty)",
|
||||
"(설명 없음)": "(no description)",
|
||||
"(지워짐)": "(deleted)",
|
||||
"(폴더 · 안의 {0}개를 지우고 같은 이름의 파일로)": "(folder and the {0} items inside are deleted and replaced by the file of the same name)",
|
||||
"(현재 방식은 옵션 없음)": "(no options for the built-in method)",
|
||||
"* 는 아무 글자, ? 는 한 글자. 여러 개는 ; 로 (예: *.srt; *.ass)": "* matches any text, ? one character. Separate several with ; (e.g. *.srt; *.ass)",
|
||||
"+ 여기에 새 줄": "+ New line here",
|
||||
|
|
@ -546,6 +548,7 @@
|
|||
"{0}개 고름": "{0} selected",
|
||||
"{0}개 더 보기": "Show {0} more",
|
||||
"{0}개 맞춤": "{0} synced",
|
||||
"{0}개 맞춤 · 같은 이름의 폴더가 있어 건너뜀 {1}개": "{0} synced · skipped {1} (a folder with the same name exists)",
|
||||
"{0}개 목록에서 숨기기": "Hide {0} items from list",
|
||||
"{0}개 보임": "{0} shown",
|
||||
"{0}개 삭제": "Delete {0} items",
|
||||
|
|
@ -657,6 +660,8 @@
|
|||
"같은 시리즈 (번호만 다른 파일)": "Same series (files that differ only by number)",
|
||||
"같은 이름을 덮어쓴 파일 {0}개는 되돌리지 않습니다.": "{0} files that replaced a file with the same name won't be undone.",
|
||||
"같은 이름의 파일이 있어 폴더를 만들 수 없습니다: {0}": "Can't create the folder because a file with the same name exists: {0}",
|
||||
"같은 이름의 폴더가 있어 건너뜀 {0}": "Skipped {0} (a folder with the same name exists)",
|
||||
"같은 이름의 폴더가 있어 건너뜀 {0}개: {1}": "Skipped {0} because a folder with the same name exists: {1}",
|
||||
"같은 이름이 이미 있습니다": "An item with the same name already exists",
|
||||
"같은 이름이 이미 있습니다: {0}": "Already exists with the same name: {0}",
|
||||
"같은 이름이 이미 있음": "Name already exists",
|
||||
|
|
|
|||
|
|
@ -59,11 +59,13 @@
|
|||
"(덮어씀)": "(上書き)",
|
||||
"(동영상을 세 번 누르면 옮깁니다)": "(動画を 3 回タップすると移動します)",
|
||||
"(링크 · junction - 따라가지 않고 건너뜀)": "(リンク · ジャンクション - たどらずにスキップ)",
|
||||
"(받는 쪽에 같은 이름의 폴더가 있어 건너뜀 - 폴더는 그대로)": "(受け取る側に同名のフォルダーがあるためスキップ - フォルダーはそのまま)",
|
||||
"(받는 쪽에만 있음)": "(受け取る側にだけある)",
|
||||
"(받는 쪽이 더 새것 - 건너뜀)": "(受け取る側が新しい - スキップ)",
|
||||
"(비어 있음)": "(空)",
|
||||
"(설명 없음)": "(説明なし)",
|
||||
"(지워짐)": "(削除)",
|
||||
"(폴더 · 안의 {0}개를 지우고 같은 이름의 파일로)": "(フォルダーと中の {0} 件を削除して同名のファイルに)",
|
||||
"(현재 방식은 옵션 없음)": "(現在の方式にはオプションなし)",
|
||||
"* 는 아무 글자, ? 는 한 글자. 여러 개는 ; 로 (예: *.srt; *.ass)": "* は任意の文字列、? は 1 文字。複数は ; で区切る (例: *.srt; *.ass)",
|
||||
"+ 여기에 새 줄": "+ ここに新しい行",
|
||||
|
|
@ -546,6 +548,7 @@
|
|||
"{0}개 고름": "{0} 件選択",
|
||||
"{0}개 더 보기": "さらに {0} 件表示",
|
||||
"{0}개 맞춤": "{0} 件を同期",
|
||||
"{0}개 맞춤 · 같은 이름의 폴더가 있어 건너뜀 {1}개": "{0} 件同期 · 同名のフォルダーがあるため {1} 件スキップ",
|
||||
"{0}개 목록에서 숨기기": "{0} 件をリストから隠す",
|
||||
"{0}개 보임": "{0} 件表示",
|
||||
"{0}개 삭제": "{0} 件を削除",
|
||||
|
|
@ -657,6 +660,8 @@
|
|||
"같은 시리즈 (번호만 다른 파일)": "同じシリーズ(番号だけ違うファイル)",
|
||||
"같은 이름을 덮어쓴 파일 {0}개는 되돌리지 않습니다.": "同じ名前を上書きしたファイル {0} 件は元に戻しません。",
|
||||
"같은 이름의 파일이 있어 폴더를 만들 수 없습니다: {0}": "同じ名前のファイルがあるためフォルダーを作成できません: {0}",
|
||||
"같은 이름의 폴더가 있어 건너뜀 {0}": "同名のフォルダーがあるためスキップ {0}",
|
||||
"같은 이름의 폴더가 있어 건너뜀 {0}개: {1}": "同名のフォルダーがあるため {0} 件スキップ: {1}",
|
||||
"같은 이름이 이미 있습니다": "同じ名前のものが既にあります",
|
||||
"같은 이름이 이미 있습니다: {0}": "同じ名前が既にあります: {0}",
|
||||
"같은 이름이 이미 있음": "同じ名前が既にあります",
|
||||
|
|
|
|||
|
|
@ -59,11 +59,13 @@
|
|||
"(덮어씀)": "(覆盖)",
|
||||
"(동영상을 세 번 누르면 옮깁니다)": "(连按视频三次会移动它)",
|
||||
"(링크 · junction - 따라가지 않고 건너뜀)": "(链接 · 联接 - 不跟随,已跳过)",
|
||||
"(받는 쪽에 같은 이름의 폴더가 있어 건너뜀 - 폴더는 그대로)": "(已跳过 - 目标有同名文件夹,文件夹保持不变)",
|
||||
"(받는 쪽에만 있음)": "(仅在接收方)",
|
||||
"(받는 쪽이 더 새것 - 건너뜀)": "(接收端较新 - 已跳过)",
|
||||
"(비어 있음)": "(空)",
|
||||
"(설명 없음)": "(无说明)",
|
||||
"(지워짐)": "(将删除)",
|
||||
"(폴더 · 안의 {0}개를 지우고 같은 이름의 파일로)": "(删除文件夹及其中 {0} 项,换成同名文件)",
|
||||
"(현재 방식은 옵션 없음)": "(当前方式没有选项)",
|
||||
"* 는 아무 글자, ? 는 한 글자. 여러 개는 ; 로 (예: *.srt; *.ass)": "* 表示任意字符,? 表示一个字符。多个用 ; 分隔(例:*.srt; *.ass)",
|
||||
"+ 여기에 새 줄": "+ 在此新增一行",
|
||||
|
|
@ -546,6 +548,7 @@
|
|||
"{0}개 고름": "已选 {0} 个",
|
||||
"{0}개 더 보기": "再显示 {0} 个",
|
||||
"{0}개 맞춤": "已同步 {0} 个",
|
||||
"{0}개 맞춤 · 같은 이름의 폴더가 있어 건너뜀 {1}개": "已同步 {0} 项 · 因同名文件夹跳过 {1} 项",
|
||||
"{0}개 목록에서 숨기기": "从列表中隐藏 {0} 项",
|
||||
"{0}개 보임": "显示 {0} 项",
|
||||
"{0}개 삭제": "删除 {0} 项",
|
||||
|
|
@ -657,6 +660,8 @@
|
|||
"같은 시리즈 (번호만 다른 파일)": "同一系列(仅编号不同的文件)",
|
||||
"같은 이름을 덮어쓴 파일 {0}개는 되돌리지 않습니다.": "{0} 个覆盖了同名文件的文件不会撤销。",
|
||||
"같은 이름의 파일이 있어 폴더를 만들 수 없습니다: {0}": "存在同名文件,无法创建文件夹: {0}",
|
||||
"같은 이름의 폴더가 있어 건너뜀 {0}": "因存在同名文件夹而跳过 {0}",
|
||||
"같은 이름의 폴더가 있어 건너뜀 {0}개: {1}": "因存在同名文件夹跳过 {0} 项: {1}",
|
||||
"같은 이름이 이미 있습니다": "已存在同名项目",
|
||||
"같은 이름이 이미 있습니다: {0}": "已存在同名项目: {0}",
|
||||
"같은 이름이 이미 있음": "同名项已存在",
|
||||
|
|
|
|||
|
|
@ -416,14 +416,17 @@ class LiveSync extends ChangeNotifier {
|
|||
if (x.delete) await ensureSourceForDelete(x.source, x.target, allowEmpty: allowEmpty);
|
||||
// 한쪽이라도 WebDAV 면 rsync · robocopy 대신 앱이 맞춘다 (크기 · 시각 비교)
|
||||
final method = isDav(x.source) || isDav(x.target) ? CopyMethod.builtin : CopyMethod.of(x.method);
|
||||
// M1 (10/19): 원격 쌍에서 받는 쪽의 같은 이름 폴더는 지우기 포함 (--delete) 이 아니면 지우지 않고 건너뛴다 - 카드에 알린다
|
||||
final ops = FileOps(bandwidthKBps: s.copyBandwidthKBps);
|
||||
final n = switch (method) {
|
||||
CopyMethod.builtin => await FileOps(bandwidthKBps: s.copyBandwidthKBps)
|
||||
.mirror(x.source, x.target, delete: x.delete, allowEmptySource: allowEmpty),
|
||||
CopyMethod.builtin => await ops.mirror(x.source, x.target, delete: x.delete, allowEmptySource: allowEmpty),
|
||||
CopyMethod.rsync => await _rsync(x, s),
|
||||
CopyMethod.robocopy => await _robocopy(x, s),
|
||||
};
|
||||
status[k] = toDelete[k] != null
|
||||
? (DateTime.now(), trf('지우지 않고 맞춤 · 대상에만 있는 {0}개를 지울지 확인 필요', [toDelete[k]!.length]))
|
||||
: ops.dirConflicts.isNotEmpty
|
||||
? (DateTime.now(), trf('{0}개 맞춤 · 같은 이름의 폴더가 있어 건너뜀 {1}개', [n < 0 ? 0 : n, ops.dirConflicts.length]))
|
||||
: (DateTime.now(), n < 0 ? tr('맞춤') : trf('{0}개 맞춤', [n]));
|
||||
problems.remove(k);
|
||||
emptyDeletes.remove(k);
|
||||
|
|
|
|||
|
|
@ -100,6 +100,9 @@ class TransferJob extends ChangeNotifier {
|
|||
/// 같은 이름이라 건너뛴 항목 (원본 경로)
|
||||
final skipped = <String>[];
|
||||
|
||||
/// M1 (10/19): 원격 맞추기에서 받는 쪽에 같은 이름의 폴더가 있어 건너뛴 것 (--delete 가 없을 때)
|
||||
final dirConflicts = <String>[];
|
||||
|
||||
TransferJob({
|
||||
required this.sources,
|
||||
required this.dest,
|
||||
|
|
@ -341,6 +344,8 @@ class TransferJob extends ChangeNotifier {
|
|||
// 폴더 "안의 것" 맞추기 (rsync 원본/ 대상/ 과 같은 뜻): -u · --delete · 원본 파일 지우기 (--remove-source-files) 도
|
||||
settled.clear();
|
||||
await _ops!.mirror(sources[i], dest, update: _update || skipNewer, delete: _delete, strict: move);
|
||||
dirConflicts.addAll(_ops!.dirConflicts);
|
||||
_ops!.dirConflicts.clear();
|
||||
if (move && !cancelled) await _deleteSettled();
|
||||
made.add(dest);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -381,6 +381,10 @@ class FileOps {
|
|||
/// 건너뛴 항목 (원본 경로)
|
||||
final skipped = <String>[];
|
||||
|
||||
/// M1 (관리자 P0, 10/19): 원격 맞추기에서 원본의 파일과 같은 이름의 **폴더** 가 받는 쪽에 있어 건너뛴 것.
|
||||
/// --delete 가 없으면 그 폴더를 지우지 않는다 (rsync 도 --delete · --force 없이는 비어 있지 않은 폴더를 지우지 않음)
|
||||
final dirConflicts = <String>[];
|
||||
|
||||
FileOps({
|
||||
this.bandwidthKBps = 0,
|
||||
this.onFileDone,
|
||||
|
|
@ -950,7 +954,15 @@ class FileOps {
|
|||
}
|
||||
}
|
||||
if (update && t != null && t.modified.isAfter(e.modified.add(const Duration(seconds: 2)))) continue;
|
||||
if (t != null && t.isDir) await vDelete(target);
|
||||
// M1 (관리자 P0, 10/19): 예전에는 --delete 가 없어도 같은 이름의 대상 폴더를 통째로 지우고 파일로 바꿨다 (미리 보기에는 "바뀜" 만).
|
||||
// 이제 --delete 일 때만 (미리 보기에 "폴더를 지우고 파일로"), 아니면 건너뛰고 알린다
|
||||
if (t != null && t.isDir) {
|
||||
if (!delete) {
|
||||
dirConflicts.add(target);
|
||||
continue;
|
||||
}
|
||||
await vDelete(target);
|
||||
}
|
||||
await _copyFileV(e.path, target, e.size, e.modified, null);
|
||||
n++;
|
||||
onSettled?.call(e.path, e.size, e.modified);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ enum PreviewAction {
|
|||
|
||||
/// 231: 원본의 링크 · junction - 앱이 직접 맞출 때는 따라가지 않고 건너뛴다 (214)
|
||||
skipLink,
|
||||
|
||||
/// M1 (10/19): 원본은 파일인데 받는 쪽에 같은 이름의 폴더가 있어 건너뜀 (--delete 가 없을 때 - 폴더를 지우지 않는다)
|
||||
skipDir,
|
||||
}
|
||||
|
||||
class PreviewItem {
|
||||
|
|
@ -36,7 +39,12 @@ class PreviewItem {
|
|||
final bool toRight;
|
||||
final bool isDir;
|
||||
final int size;
|
||||
const PreviewItem(this.rel, this.action, {required this.toRight, this.isDir = false, this.size = 0, this.newer = false});
|
||||
const PreviewItem(this.rel, this.action,
|
||||
{required this.toRight, this.isDir = false, this.size = 0, this.newer = false, this.replacedByFile = false, this.inner = 0});
|
||||
|
||||
/// M1 (10/19): [PreviewAction.delete] 인 폴더가 원본의 같은 이름 파일로 바뀜 (--delete) - 그 폴더 안의 항목 수 [inner]
|
||||
final bool replacedByFile;
|
||||
final int inner;
|
||||
|
||||
/// 229: [PreviewAction.update] 인데 받는 쪽이 더 새것 (예전 원본으로 새 파일을 덮어씀)
|
||||
final bool newer;
|
||||
|
|
@ -133,6 +141,17 @@ Future<List<PreviewItem>> previewSync(String src, String dst,
|
|||
await walk(e.path, vJoin(d, name), r);
|
||||
continue;
|
||||
}
|
||||
// M1 (관리자 P0, 10/19): 받는 쪽의 같은 이름이 폴더 - --delete 면 폴더 (안의 것 모두) 를 지우고 파일로, 아니면 건너뜀 (앱 맞추기 · rsync 같음)
|
||||
if (t != null && t.isDir) {
|
||||
if (delete) {
|
||||
out.add(PreviewItem(r, PreviewAction.delete,
|
||||
toRight: toRight, isDir: true, replacedByFile: true, inner: await _countInside(t.path)));
|
||||
out.add(PreviewItem(r, PreviewAction.add, toRight: toRight, size: e.size));
|
||||
} else {
|
||||
out.add(PreviewItem(r, PreviewAction.skipDir, toRight: toRight, size: e.size));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (t == null) {
|
||||
out.add(PreviewItem(r, PreviewAction.add, toRight: toRight, size: e.size));
|
||||
} else if (t.size != e.size || t.modified.difference(e.modified).inSeconds.abs() > 2) {
|
||||
|
|
@ -175,3 +194,24 @@ Future<List<PreviewItem>> previewBoth(String left, String right, {bool delete =
|
|||
x,
|
||||
];
|
||||
}
|
||||
|
||||
/// M1: 폴더 [dir] 안의 모든 항목 수 (지우고 파일로 바뀔 때 보이려고, [limit] 에서 멈춤)
|
||||
Future<int> _countInside(String dir, {int limit = 100000}) async {
|
||||
var n = 0;
|
||||
Future<void> go(String d) async {
|
||||
List<({String path, bool isDir, int size, DateTime modified})> items;
|
||||
try {
|
||||
items = await vList(d);
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
for (final x in items) {
|
||||
if (n >= limit) return;
|
||||
n++;
|
||||
if (x.isDir) await go(x.path);
|
||||
}
|
||||
}
|
||||
|
||||
await go(dir);
|
||||
return n;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3306,6 +3306,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
if (move && job.leftInSource > 0) trf('{0}개는 원본에 남음 (건너뜀)', [job.leftInSource]),
|
||||
if (!move && job.keptNewer > 0) trf('받는 쪽이 더 새것 {0}개 건너뜀', [job.keptNewer]),
|
||||
if (job.skipped.isNotEmpty) trf('같은 이름이라 건너뜀: {0}', [_namesOf(job.skipped)]),
|
||||
// M1 (10/19): 원격 맞추기에서 받는 쪽의 같은 이름 폴더는 --delete 없이 지우지 않는다
|
||||
if (job.dirConflicts.isNotEmpty) trf('같은 이름의 폴더가 있어 건너뜀 {0}개: {1}', [job.dirConflicts.length, _namesOf(job.dirConflicts)]),
|
||||
if (skippedHere.isNotEmpty) trf('이미 이 폴더에 있어 건너뜀: {0}', [_namesOf(skippedHere)]),
|
||||
].join(' · ');
|
||||
// 85: 끝나면 [폴더 열기] - 앱 탐색기에서 넣은 폴더로 (그 폴더를 보고 있는 창, 없으면 지금 창)
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ class _SyncPreviewViewState extends State<SyncPreviewView> {
|
|||
final overNewer = items.where((x) => x.action == PreviewAction.update && x.newer).length;
|
||||
final skipped = n(PreviewAction.skipNewer);
|
||||
final links = n(PreviewAction.skipLink);
|
||||
final dirSkips = n(PreviewAction.skipDir);
|
||||
final more = items.length >= widget.limit;
|
||||
final parts = [
|
||||
for (final right in [true, false])
|
||||
|
|
@ -109,6 +110,7 @@ class _SyncPreviewViewState extends State<SyncPreviewView> {
|
|||
if (n(PreviewAction.onlyTarget) > 0) trf('받는 쪽에만 있음 {0} (그대로 둠)', [n(PreviewAction.onlyTarget)]),
|
||||
if (skipped > 0) trf('받는 쪽이 더 새것 {0} (건너뜀)', [skipped]),
|
||||
if (links > 0) trf('링크 {0} (건너뜀)', [links]),
|
||||
if (dirSkips > 0) trf('같은 이름의 폴더가 있어 건너뜀 {0}', [dirSkips]),
|
||||
];
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
|
||||
Row(children: [
|
||||
|
|
@ -153,14 +155,17 @@ class _SyncPreviewViewState extends State<SyncPreviewView> {
|
|||
PreviewAction.skipNewer => ' · ${x.rel} ${tr('(받는 쪽이 더 새것 - 건너뜀)')}',
|
||||
PreviewAction.skipLink => ' · ${x.rel}${x.isDir ? '/' : ''} ${tr('(링크 · junction - 따라가지 않고 건너뜀)')}',
|
||||
PreviewAction.onlyTarget => ' = ${x.rel}${x.isDir ? '/' : ''} ${tr('(받는 쪽에만 있음)')}',
|
||||
PreviewAction.delete => '${_arrow(x)} − ${x.rel}${x.isDir ? '/' : ''} ${tr('(지워짐)')}',
|
||||
PreviewAction.delete => x.replacedByFile
|
||||
? '${_arrow(x)} − ${x.rel}/ ${trf('(폴더 · 안의 {0}개를 지우고 같은 이름의 파일로)', [x.inner])}'
|
||||
: '${_arrow(x)} − ${x.rel}${x.isDir ? '/' : ''} ${tr('(지워짐)')}',
|
||||
PreviewAction.skipDir => ' · ${x.rel} ${tr('(받는 쪽에 같은 이름의 폴더가 있어 건너뜀 - 폴더는 그대로)')}',
|
||||
},
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
fontFamily: 'monospace',
|
||||
color: switch (x.action) {
|
||||
PreviewAction.delete => Colors.redAccent,
|
||||
PreviewAction.update => Colors.orangeAccent,
|
||||
PreviewAction.update || PreviewAction.skipDir => Colors.orangeAccent,
|
||||
PreviewAction.onlyTarget || PreviewAction.skipNewer || PreviewAction.skipLink => JjColors.textDim,
|
||||
_ => null,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import 'package:jj_mkvmaker/services/secret_store.dart';
|
|||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/sftp_server.dart';
|
||||
import 'package:jj_mkvmaker/core/file_ops.dart';
|
||||
import 'package:jj_mkvmaker/core/sync_preview.dart';
|
||||
|
||||
/// 88: SFTP (dartssh2). 실제 서버 시험은 tool/sftp_test_server.py (paramiko) 를 띄운다 -
|
||||
/// 파이썬 (JJ_SFTP_PY, 없으면 ..\tools\sftp_test_py 의 venv) 이 없으면 건너뛴다.
|
||||
|
|
@ -181,6 +183,27 @@ void main() {
|
|||
expect((off! as SftpException).kind, SftpFailure.unreachable);
|
||||
}, skip: hasServer ? false : 'SFTP 시험 서버 (paramiko) 없음');
|
||||
|
||||
test('M1 (관리자 P0): SFTP 로 맞출 때 받는 쪽 같은 이름 폴더 - 지우기 없으면 그대로 · 지우기면 지우고 파일로 (미리 보기 같음)', () async {
|
||||
await start();
|
||||
SftpRegistry.configure([srv(fp: fingerprint)]);
|
||||
final local = Directory(p.join(tmp.path, 'local', 'src'))..createSync(recursive: true);
|
||||
File(p.join(local.path, 'x')).writeAsStringSync('file');
|
||||
Directory(p.join(root.path, 'dst', 'x', 'sub')).createSync(recursive: true);
|
||||
File(p.join(root.path, 'dst', 'x', 'keep.txt')).writeAsStringSync('k');
|
||||
const dst = 'sftp://s1/dst';
|
||||
var pv = await previewSync(local.path, dst, toRight: true);
|
||||
expect(pv.where((x) => x.action == PreviewAction.skipDir).map((x) => x.rel), ['x']);
|
||||
final ops = FileOps();
|
||||
await ops.mirror(local.path, dst);
|
||||
expect(File(p.join(root.path, 'dst', 'x', 'keep.txt')).existsSync(), isTrue);
|
||||
expect(ops.dirConflicts, hasLength(1));
|
||||
pv = await previewSync(local.path, dst, toRight: true, delete: true);
|
||||
final del = pv.singleWhere((x) => x.action == PreviewAction.delete);
|
||||
expect((del.replacedByFile, del.inner), (true, 2));
|
||||
await FileOps().mirror(local.path, dst, delete: true);
|
||||
expect(File(p.join(root.path, 'dst', 'x')).readAsStringSync(), 'file');
|
||||
}, skip: hasServer ? false : 'SFTP 시험 서버 (paramiko) 없음');
|
||||
|
||||
test('vfs 로 폴더 · 쓰기 (덮어쓰기) · 목록 · 읽기 · 옮기기 · 지우기 (안쪽까지) · 시작 폴더', () async {
|
||||
await start();
|
||||
SftpRegistry.configure([srv(fp: fingerprint)]);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ 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;
|
||||
|
|
@ -190,4 +191,30 @@ void main() {
|
|||
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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue