미리 보기: 원본의 링크 · junction 은 따라가지 않음 (231)
- 버그: 미리 보기가 링크 안으로 들어가 실제로는 건너뛰는 (214) 바깥 파일을 옮길 것으로 보였다 - 앱이 직접 (현재 방식 · WebDAV) 이면 '링크 · junction - 따라가지 않고 건너뜀', rsync 는 링크 하나로 셈 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>fix_mkv
parent
41d4d8afbf
commit
d86a929ba1
|
|
@ -49,6 +49,7 @@
|
||||||
"(더 새 파일을 덮어씀)": "(overwrites a newer file)",
|
"(더 새 파일을 덮어씀)": "(overwrites a newer file)",
|
||||||
"(덮어씀)": "(overwritten)",
|
"(덮어씀)": "(overwritten)",
|
||||||
"(동영상을 세 번 누르면 옮깁니다)": "(Tapping a video three times moves it)",
|
"(동영상을 세 번 누르면 옮깁니다)": "(Tapping a video three times moves it)",
|
||||||
|
"(링크 · junction - 따라가지 않고 건너뜀)": "(link · junction - not followed, skipped)",
|
||||||
"(받는 쪽에만 있음)": "(only on receiving side)",
|
"(받는 쪽에만 있음)": "(only on receiving side)",
|
||||||
"(받는 쪽이 더 새것 - 건너뜀)": "(receiving side is newer - skipped)",
|
"(받는 쪽이 더 새것 - 건너뜀)": "(receiving side is newer - skipped)",
|
||||||
"(비어 있음)": "(empty)",
|
"(비어 있음)": "(empty)",
|
||||||
|
|
@ -787,6 +788,7 @@
|
||||||
"로그인이 필요합니다 (403)": "Login required (403)",
|
"로그인이 필요합니다 (403)": "Login required (403)",
|
||||||
"로그인하면 하루 받기 횟수가 늘어납니다": "Signing in raises the daily download limit",
|
"로그인하면 하루 받기 횟수가 늘어납니다": "Signing in raises the daily download limit",
|
||||||
"루마니아어": "Romanian",
|
"루마니아어": "Romanian",
|
||||||
|
"링크 {0} (건너뜀)": "Links {0} (skipped)",
|
||||||
"마스터 · 최상 비밀번호": "Master and super passwords",
|
"마스터 · 최상 비밀번호": "Master and super passwords",
|
||||||
"마스터 비밀번호": "Master password",
|
"마스터 비밀번호": "Master password",
|
||||||
"마스터 비밀번호 넣기": "Enter master password",
|
"마스터 비밀번호 넣기": "Enter master password",
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
"(더 새 파일을 덮어씀)": "(新しいファイルを上書き)",
|
"(더 새 파일을 덮어씀)": "(新しいファイルを上書き)",
|
||||||
"(덮어씀)": "(上書き)",
|
"(덮어씀)": "(上書き)",
|
||||||
"(동영상을 세 번 누르면 옮깁니다)": "(動画を 3 回タップすると移動します)",
|
"(동영상을 세 번 누르면 옮깁니다)": "(動画を 3 回タップすると移動します)",
|
||||||
|
"(링크 · junction - 따라가지 않고 건너뜀)": "(リンク · ジャンクション - たどらずにスキップ)",
|
||||||
"(받는 쪽에만 있음)": "(受け取る側にだけある)",
|
"(받는 쪽에만 있음)": "(受け取る側にだけある)",
|
||||||
"(받는 쪽이 더 새것 - 건너뜀)": "(受け取る側が新しい - スキップ)",
|
"(받는 쪽이 더 새것 - 건너뜀)": "(受け取る側が新しい - スキップ)",
|
||||||
"(비어 있음)": "(空)",
|
"(비어 있음)": "(空)",
|
||||||
|
|
@ -787,6 +788,7 @@
|
||||||
"로그인이 필요합니다 (403)": "ログインが必要です (403)",
|
"로그인이 필요합니다 (403)": "ログインが必要です (403)",
|
||||||
"로그인하면 하루 받기 횟수가 늘어납니다": "ログインすると 1日のダウンロード回数が増えます",
|
"로그인하면 하루 받기 횟수가 늘어납니다": "ログインすると 1日のダウンロード回数が増えます",
|
||||||
"루마니아어": "ルーマニア語",
|
"루마니아어": "ルーマニア語",
|
||||||
|
"링크 {0} (건너뜀)": "リンク {0} (スキップ)",
|
||||||
"마스터 · 최상 비밀번호": "マスター / 最上位パスワード",
|
"마스터 · 최상 비밀번호": "マスター / 最上位パスワード",
|
||||||
"마스터 비밀번호": "マスターパスワード",
|
"마스터 비밀번호": "マスターパスワード",
|
||||||
"마스터 비밀번호 넣기": "マスターパスワードを入力",
|
"마스터 비밀번호 넣기": "マスターパスワードを入力",
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
"(더 새 파일을 덮어씀)": "(覆盖较新的文件)",
|
"(더 새 파일을 덮어씀)": "(覆盖较新的文件)",
|
||||||
"(덮어씀)": "(覆盖)",
|
"(덮어씀)": "(覆盖)",
|
||||||
"(동영상을 세 번 누르면 옮깁니다)": "(连按视频三次会移动它)",
|
"(동영상을 세 번 누르면 옮깁니다)": "(连按视频三次会移动它)",
|
||||||
|
"(링크 · junction - 따라가지 않고 건너뜀)": "(链接 · 联接 - 不跟随,已跳过)",
|
||||||
"(받는 쪽에만 있음)": "(仅在接收方)",
|
"(받는 쪽에만 있음)": "(仅在接收方)",
|
||||||
"(받는 쪽이 더 새것 - 건너뜀)": "(接收端较新 - 已跳过)",
|
"(받는 쪽이 더 새것 - 건너뜀)": "(接收端较新 - 已跳过)",
|
||||||
"(비어 있음)": "(空)",
|
"(비어 있음)": "(空)",
|
||||||
|
|
@ -787,6 +788,7 @@
|
||||||
"로그인이 필요합니다 (403)": "需要登录 (403)",
|
"로그인이 필요합니다 (403)": "需要登录 (403)",
|
||||||
"로그인하면 하루 받기 횟수가 늘어납니다": "登录后每日下载次数会增加",
|
"로그인하면 하루 받기 횟수가 늘어납니다": "登录后每日下载次数会增加",
|
||||||
"루마니아어": "罗马尼亚语",
|
"루마니아어": "罗马尼亚语",
|
||||||
|
"링크 {0} (건너뜀)": "链接 {0}(跳过)",
|
||||||
"마스터 · 최상 비밀번호": "主密码和最高密码",
|
"마스터 · 최상 비밀번호": "主密码和最高密码",
|
||||||
"마스터 비밀번호": "主密码",
|
"마스터 비밀번호": "主密码",
|
||||||
"마스터 비밀번호 넣기": "输入主密码",
|
"마스터 비밀번호 넣기": "输入主密码",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:io' show FileSystemEntity;
|
||||||
|
|
||||||
import 'file_ops.dart' show isPartialFile, SourceUnreadableException;
|
import 'file_ops.dart' show isPartialFile, SourceUnreadableException;
|
||||||
import 'sync_tools.dart' show splitOptions, isSourceRemovingOption;
|
import 'sync_tools.dart' show splitOptions, isSourceRemovingOption;
|
||||||
|
|
||||||
|
|
@ -20,6 +22,9 @@ enum PreviewAction {
|
||||||
|
|
||||||
/// 229: 받는 쪽이 더 새것이라 건너뜀 (-u · 환경 설정 "받는 쪽이 더 새것이면 건너뛰기")
|
/// 229: 받는 쪽이 더 새것이라 건너뜀 (-u · 환경 설정 "받는 쪽이 더 새것이면 건너뛰기")
|
||||||
skipNewer,
|
skipNewer,
|
||||||
|
|
||||||
|
/// 231: 원본의 링크 · junction - 앱이 직접 맞출 때는 따라가지 않고 건너뛴다 (214)
|
||||||
|
skipLink,
|
||||||
}
|
}
|
||||||
|
|
||||||
class PreviewItem {
|
class PreviewItem {
|
||||||
|
|
@ -86,8 +91,10 @@ bool optionsUpdate(String options) =>
|
||||||
|
|
||||||
/// [src] 안의 것 → [dst] 안으로 맞출 때 바뀌는 것. 원본을 읽지 못한 폴더에서는 "지워짐" 을 세지 않는다.
|
/// [src] 안의 것 → [dst] 안으로 맞출 때 바뀌는 것. 원본을 읽지 못한 폴더에서는 "지워짐" 을 세지 않는다.
|
||||||
/// [limit] 개를 넘으면 멈춘다 (화면에는 "… 이상").
|
/// [limit] 개를 넘으면 멈춘다 (화면에는 "… 이상").
|
||||||
|
/// 231: 원본의 링크 · junction 은 안으로 들어가지 않는다. [skipLinks] (앱이 직접 - 현재 방식 · WebDAV) 면 "건너뜀 (링크)",
|
||||||
|
/// 아니면 (rsync -a · robocopy) 링크 자체 하나로 센다
|
||||||
Future<List<PreviewItem>> previewSync(String src, String dst,
|
Future<List<PreviewItem>> previewSync(String src, String dst,
|
||||||
{required bool toRight, bool delete = false, bool update = false, int limit = 2000}) async {
|
{required bool toRight, bool delete = false, bool update = false, bool skipLinks = false, int limit = 2000}) async {
|
||||||
final out = <PreviewItem>[];
|
final out = <PreviewItem>[];
|
||||||
Future<void> walk(String s, String d, String rel) async {
|
Future<void> walk(String s, String d, String rel) async {
|
||||||
if (out.length >= limit) return;
|
if (out.length >= limit) return;
|
||||||
|
|
@ -113,6 +120,14 @@ Future<List<PreviewItem>> previewSync(String src, String dst,
|
||||||
names.add(name);
|
names.add(name);
|
||||||
final r = rel.isEmpty ? name : '$rel/$name';
|
final r = rel.isEmpty ? name : '$rel/$name';
|
||||||
final t = there[name];
|
final t = there[name];
|
||||||
|
if (!isDav(e.path) && FileSystemEntity.isLinkSync(e.path)) {
|
||||||
|
if (skipLinks) {
|
||||||
|
out.add(PreviewItem(r, PreviewAction.skipLink, toRight: toRight, isDir: e.isDir));
|
||||||
|
} else if (t == null) {
|
||||||
|
out.add(PreviewItem(r, PreviewAction.add, toRight: toRight));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (e.isDir) {
|
if (e.isDir) {
|
||||||
if (t == null) out.add(PreviewItem(r, PreviewAction.add, toRight: toRight, isDir: true));
|
if (t == null) out.add(PreviewItem(r, PreviewAction.add, toRight: toRight, isDir: true));
|
||||||
await walk(e.path, vJoin(d, name), r);
|
await walk(e.path, vJoin(d, name), r);
|
||||||
|
|
|
||||||
|
|
@ -2166,7 +2166,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
toRight: t.sources.first == l,
|
toRight: t.sources.first == l,
|
||||||
delete: deletes(),
|
delete: deletes(),
|
||||||
// 229: WebDAV 가 끼면 앱이 직접 - 환경 설정 "받는 쪽이 더 새것이면 건너뛰기" 도 따른다
|
// 229: WebDAV 가 끼면 앱이 직접 - 환경 설정 "받는 쪽이 더 새것이면 건너뛰기" 도 따른다
|
||||||
update: optionsUpdate(opts(t)) || (_viaDav([l, r]) && c.settings.copySkipNewer)),
|
update: optionsUpdate(opts(t)) || (_viaDav([l, r]) && c.settings.copySkipNewer),
|
||||||
|
skipLinks: _viaDav([l, r])),
|
||||||
],
|
],
|
||||||
// 97: 다시 비교하는 동안은 예전 결과로 실행하지 못하게
|
// 97: 다시 비교하는 동안은 예전 결과로 실행하지 못하게
|
||||||
onStart: () => set(() {
|
onStart: () => set(() {
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ class _SyncPreviewViewState extends State<SyncPreviewView> {
|
||||||
final overwrites = n(PreviewAction.update);
|
final overwrites = n(PreviewAction.update);
|
||||||
final overNewer = items.where((x) => x.action == PreviewAction.update && x.newer).length;
|
final overNewer = items.where((x) => x.action == PreviewAction.update && x.newer).length;
|
||||||
final skipped = n(PreviewAction.skipNewer);
|
final skipped = n(PreviewAction.skipNewer);
|
||||||
|
final links = n(PreviewAction.skipLink);
|
||||||
final more = items.length >= widget.limit;
|
final more = items.length >= widget.limit;
|
||||||
final parts = [
|
final parts = [
|
||||||
for (final right in [true, false])
|
for (final right in [true, false])
|
||||||
|
|
@ -107,6 +108,7 @@ class _SyncPreviewViewState extends State<SyncPreviewView> {
|
||||||
trf('{0} 새로 {1} · 덮어씀 {2}', [right ? '→' : '←', n(PreviewAction.add, right), n(PreviewAction.update, right)]),
|
trf('{0} 새로 {1} · 덮어씀 {2}', [right ? '→' : '←', n(PreviewAction.add, right), n(PreviewAction.update, right)]),
|
||||||
if (n(PreviewAction.onlyTarget) > 0) trf('받는 쪽에만 있음 {0} (그대로 둠)', [n(PreviewAction.onlyTarget)]),
|
if (n(PreviewAction.onlyTarget) > 0) trf('받는 쪽에만 있음 {0} (그대로 둠)', [n(PreviewAction.onlyTarget)]),
|
||||||
if (skipped > 0) trf('받는 쪽이 더 새것 {0} (건너뜀)', [skipped]),
|
if (skipped > 0) trf('받는 쪽이 더 새것 {0} (건너뜀)', [skipped]),
|
||||||
|
if (links > 0) trf('링크 {0} (건너뜀)', [links]),
|
||||||
];
|
];
|
||||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
|
return Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
|
||||||
Row(children: [
|
Row(children: [
|
||||||
|
|
@ -149,6 +151,7 @@ class _SyncPreviewViewState extends State<SyncPreviewView> {
|
||||||
PreviewAction.update =>
|
PreviewAction.update =>
|
||||||
'${_arrow(x)} ~ ${x.rel} (${formatSize(x.size)}) ${x.newer ? tr('(더 새 파일을 덮어씀)') : tr('(덮어씀)')}',
|
'${_arrow(x)} ~ ${x.rel} (${formatSize(x.size)}) ${x.newer ? tr('(더 새 파일을 덮어씀)') : tr('(덮어씀)')}',
|
||||||
PreviewAction.skipNewer => ' · ${x.rel} ${tr('(받는 쪽이 더 새것 - 건너뜀)')}',
|
PreviewAction.skipNewer => ' · ${x.rel} ${tr('(받는 쪽이 더 새것 - 건너뜀)')}',
|
||||||
|
PreviewAction.skipLink => ' · ${x.rel}${x.isDir ? '/' : ''} ${tr('(링크 · junction - 따라가지 않고 건너뜀)')}',
|
||||||
PreviewAction.onlyTarget => ' = ${x.rel}${x.isDir ? '/' : ''} ${tr('(받는 쪽에만 있음)')}',
|
PreviewAction.onlyTarget => ' = ${x.rel}${x.isDir ? '/' : ''} ${tr('(받는 쪽에만 있음)')}',
|
||||||
PreviewAction.delete => '${_arrow(x)} − ${x.rel}${x.isDir ? '/' : ''} ${tr('(지워짐)')}',
|
PreviewAction.delete => '${_arrow(x)} − ${x.rel}${x.isDir ? '/' : ''} ${tr('(지워짐)')}',
|
||||||
},
|
},
|
||||||
|
|
@ -158,7 +161,7 @@ class _SyncPreviewViewState extends State<SyncPreviewView> {
|
||||||
color: switch (x.action) {
|
color: switch (x.action) {
|
||||||
PreviewAction.delete => Colors.redAccent,
|
PreviewAction.delete => Colors.redAccent,
|
||||||
PreviewAction.update => Colors.orangeAccent,
|
PreviewAction.update => Colors.orangeAccent,
|
||||||
PreviewAction.onlyTarget || PreviewAction.skipNewer => JjColors.textDim,
|
PreviewAction.onlyTarget || PreviewAction.skipNewer || PreviewAction.skipLink => JjColors.textDim,
|
||||||
_ => null,
|
_ => null,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -243,7 +246,7 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
SyncPreviewView(
|
SyncPreviewView(
|
||||||
compute: () async => [
|
compute: () async => [
|
||||||
for (final s in t.sources) ...await previewSync(s, dstOf(s), toRight: true, delete: deletes, update: update),
|
for (final s in t.sources) ...await previewSync(s, dstOf(s), toRight: true, delete: deletes, update: update, skipLinks: direct),
|
||||||
],
|
],
|
||||||
onStart: () => set(() {
|
onStart: () => set(() {
|
||||||
preview = null;
|
preview = null;
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,20 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('231: 원본의 링크 · junction 은 안으로 들어가지 않는다 - 앱이 직접이면 "건너뜀 (링크)", rsync 면 링크 하나', () async {
|
||||||
|
final outside = p.join(tmp.path, 'outside');
|
||||||
|
f(outside, 'big.bin', 'x', old);
|
||||||
|
Link(p.join(a, 'linked')).createSync(outside); // Windows 는 junction
|
||||||
|
final direct = await previewSync(a, b, toRight: true, skipLinks: true);
|
||||||
|
expect([for (final x in direct) if (x.rel.startsWith('linked')) (x.rel, x.action)], [('linked', PreviewAction.skipLink)]);
|
||||||
|
final viaRsync = await previewSync(a, b, toRight: true);
|
||||||
|
expect([for (final x in viaRsync) if (x.rel.startsWith('linked')) (x.rel, x.action)], [('linked', PreviewAction.add)]);
|
||||||
|
// 받는 쪽에 같은 이름이 있으면 지울 것으로 세지 않는다 (맞추기도 지우지 않음 - 214)
|
||||||
|
Directory(p.join(b, 'linked')).createSync();
|
||||||
|
final del = await previewSync(a, b, toRight: true, delete: true, skipLinks: true);
|
||||||
|
expect([for (final x in del) if (x.rel == 'linked') x.action], [PreviewAction.skipLink]);
|
||||||
|
});
|
||||||
|
|
||||||
test('93: 원본 폴더를 읽지 못하면 비교 실패 (지우는 실행을 막는다)', () async {
|
test('93: 원본 폴더를 읽지 못하면 비교 실패 (지우는 실행을 막는다)', () async {
|
||||||
await expectLater(previewSync(p.join(tmp.path, 'none'), b, toRight: true, delete: true),
|
await expectLater(previewSync(p.join(tmp.path, 'none'), b, toRight: true, delete: true),
|
||||||
throwsA(isA<SourceUnreadableException>()));
|
throwsA(isA<SourceUnreadableException>()));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue