modular_sync F2: "같은 파일" 판단을 SameFile 하나로 - 실행 전 미리 보기 = 실제 맞추기
- lib/core/files/same_file.dart: 크기 + 바뀐 때 ±2초, 받는 쪽 시각이 올린 때인 곳 (원격이 낀 앱 맞추기, 이동 아님) 은 받는 쪽이 더 새것도 같음. SameFile.forAppSync(remote, move) · targetNewer - 부르는 곳: 로컬 맞추기 (local_transfer) · 원격 맞추기 (any_transfer) · 미리 보기 (sync_preview) · 실시간 동기화 기다림 (sync_diff) · -u 건너뛰기 · 폴더 합칠 때 더 새것 지키기 - 고친 어긋남 (15 검토 F2): 원격으로 앱이 직접 맞출 때 받는 쪽이 같은 크기에 더 새 파일이면 미리 보기에 "바뀜" 으로 나왔는데 실제로는 보내지 않았다. 이제 미리 보기도 실제와 같은 판단 (rsync_runner viaDav · sync_preview_view direct 가 SameFile.forAppSync 를 넘김) - 약속 시험: 판단 표 + 실제 파일로 미리 보기와 실제 맞추기 (anyMirror · localMirror) 가 같은지 - 원격 규칙 · 이동 · 로컬 사용자가 보게 될 동작: 원격으로 앱이 직접 맞출 때 받는 쪽의 같은 크기 · 더 새 파일이 미리 보기에서 빠짐 (실제로도 보내지 않던 것). 로컬 · rsync · 이동은 그대로 확인: analyze 0 · 단위 1479 ×2 · 통합 6/6 ×2 · base10 214장 0px · rsync_page · copy_sync_shot · lsync_p0_shot · live_excl_shot ×2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
e89a0ff2d7
commit
88f0701d23
|
|
@ -6,6 +6,7 @@ import '../l10n/tr.dart';
|
|||
import 'copy_center.dart';
|
||||
import 'settings.dart' show CopyTask;
|
||||
import 'transfer_job.dart';
|
||||
import '../core/files/same_file.dart';
|
||||
|
||||
/// 약속 (E5c): Rsync 화면의 [→] · [←] · [⇄] 실행. 화면 (확인 창) 은 이 객체만 부른다.
|
||||
/// rsync 옵션 · 미리 보기 · 실행은 sync_preview · sync_tools · rsync_filter · CopyCenter 를 부르기만 한다 (rsync 를 흉내 내지 않음).
|
||||
|
|
@ -123,6 +124,8 @@ class RsyncRun {
|
|||
delete: deletes(),
|
||||
update: optionsUpdate(opts(t)) || (viaDav && copySkipNewer),
|
||||
skipLinks: viaDav,
|
||||
// F2: 앱이 직접 (원격) 맞출 때는 실제 맞추기와 같은 판단 (이동이면 거의 같은 때만)
|
||||
same: viaDav ? SameFile.forAppSync(remote: true, move: optionMove) : const SameFile(),
|
||||
filter: RsyncFilter.parse(opts(t))),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import '../../core/file_ops.dart';
|
|||
import '../../core/rsync_filter.dart';
|
||||
import '../../core/vfs.dart';
|
||||
import '../settings.dart';
|
||||
import '../../core/files/same_file.dart';
|
||||
|
||||
class SyncDiff {
|
||||
/// 받는 쪽에만 있는 [path] (상대 경로 [r]) 가 지워지는 모양을 [out] 에 "− 경로" 로 (rsync · _mirrorV 와 같게):
|
||||
|
|
@ -100,7 +101,7 @@ class SyncDiff {
|
|||
continue;
|
||||
}
|
||||
final ts = await tf.stat();
|
||||
if (ts.size != st.size || ts.modified.difference(st.modified).inSeconds.abs() > 2) out.add(r);
|
||||
if (!const SameFile().same(size: st.size, modified: st.modified, targetSize: ts.size, targetModified: ts.modified)) out.add(r);
|
||||
}
|
||||
}
|
||||
if (x.delete && await Directory(dst).exists()) {
|
||||
|
|
@ -149,7 +150,8 @@ class SyncDiff {
|
|||
final t = there[name];
|
||||
// M1: 받는 쪽이 같은 이름의 폴더면 지우기 포함일 때만 바뀐다 (아니면 건너뜀 - 기다리는 것으로 세지 않음)
|
||||
if (t != null && t.isDir && !x.delete) continue;
|
||||
if (t == null || t.isDir || t.size != e.size || t.modified.isBefore(e.modified.subtract(const Duration(seconds: 2)))) {
|
||||
// 원격 맞추기와 같은 판단 (SameFile - 받는 쪽 시각이 올린 때라 더 새것도 같음)
|
||||
if (t == null || t.isDir || !const SameFile(uploadTime: true).same(size: e.size, modified: e.modified, targetSize: t.size, targetModified: t.modified)) {
|
||||
out.add(r);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import '../vfs.dart';
|
|||
import 'path_rules.dart';
|
||||
import 'transfer_control.dart';
|
||||
import 'transfer_guard.dart';
|
||||
import 'same_file.dart';
|
||||
|
||||
// 약속 (나누기 file_ops): 어느 저장소든 (로컬 · WebDAV · SFTP · FTP 등 vfs 가 아는 곳) 의 복사 · 이동 · 맞추기 · 크기.
|
||||
// 한쪽이라도 원격이면 [FileOps] 가 이쪽을 부른다. 같은 서버 안의 이동은 서버가 옮긴다.
|
||||
|
|
@ -94,7 +95,7 @@ Future<List<String>> anyCopy(TransferControl c, List<String> sources, String des
|
|||
// 229: 합치는 폴더 안에서 받는 쪽이 더 새것이면 예전 것으로 덮지 않는다 (WebDAV 도 로컬과 같게)
|
||||
if (inner && c.skipNewerInMerge) {
|
||||
final t = await vStat(target);
|
||||
if (t != null && !t.isDir && t.modified.isAfter(st.modified.add(const Duration(seconds: 2)))) {
|
||||
if (t != null && !t.isDir && SameFile.targetNewer(t.modified, st.modified)) {
|
||||
c.keptNewer.add(src);
|
||||
return;
|
||||
}
|
||||
|
|
@ -170,6 +171,7 @@ Future<bool> _deleteExcept(String dir, Set<String> keep) async {
|
|||
Future<int> anyMirror(TransferControl c, String srcDir, String dstDir,
|
||||
{bool delete = false, bool update = false, bool strict = false, RsyncFilter filter = RsyncFilter.none}) async {
|
||||
var n = 0;
|
||||
final same = SameFile(uploadTime: !strict);
|
||||
// 관리자 지시 (10/19): 거르기는 rsync 와 같게 (미리 보기 sync_preview 와 같은 규칙) - 거른 것은 보내지 않고 받는 쪽의 거른 것은 지우지 않는다
|
||||
// (--delete-excluded 면 지운다). 받는 쪽에만 있는 폴더 안에 거른 것이 있으면 폴더는 남기고 거르지 않은 것만 지운다
|
||||
Future<bool> hasExcluded(String dir, String rel) async {
|
||||
|
|
@ -228,16 +230,13 @@ Future<int> anyMirror(TransferControl c, String srcDir, String dstDir,
|
|||
continue;
|
||||
}
|
||||
final t = there[name];
|
||||
if (t != null && !t.isDir && t.size == e.size) {
|
||||
final close = t.modified.difference(e.modified).inSeconds.abs() <= 2;
|
||||
// 크기가 같고 대상이 원본보다 새것이거나 (올린 파일은 서버 시각) 거의 같으면 그대로.
|
||||
// 214: 이동 ([strict]) 은 거의 같은 시각일 때만 같다고 본다 (아니면 다시 보내고 지운다 - 내용이 다른데 지우지 않게)
|
||||
if (close || (!strict && !t.modified.isBefore(e.modified.subtract(const Duration(seconds: 2))))) {
|
||||
c.onSettled?.call(e.path, e.size, e.modified);
|
||||
continue;
|
||||
}
|
||||
// 같은 파일 판단은 SameFile 하나 (미리 보기와 같음): 크기가 같고 대상이 원본보다 새것이거나 (올린 파일은 서버 시각) 거의 같으면 그대로.
|
||||
// 214: 이동 ([strict]) 은 거의 같은 시각일 때만 같다고 본다 (아니면 다시 보내고 지운다 - 내용이 다른데 지우지 않게)
|
||||
if (t != null && !t.isDir && same.same(size: e.size, modified: e.modified, targetSize: t.size, targetModified: t.modified)) {
|
||||
c.onSettled?.call(e.path, e.size, e.modified);
|
||||
continue;
|
||||
}
|
||||
if (update && t != null && t.modified.isAfter(e.modified.add(const Duration(seconds: 2)))) continue;
|
||||
if (update && t != null && SameFile.targetNewer(t.modified, e.modified)) continue;
|
||||
// M1 (관리자 P0, 10/19): 예전에는 --delete 가 없어도 같은 이름의 대상 폴더를 통째로 지우고 파일로 바꿨다 (미리 보기에는 "바뀜" 만).
|
||||
// 이제 --delete 일 때만 (미리 보기에 "폴더를 지우고 파일로"), 아니면 건너뛰고 알린다
|
||||
if (t != null && t.isDir) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import '../vfs.dart';
|
|||
import 'path_rules.dart';
|
||||
import 'transfer_control.dart';
|
||||
import 'transfer_guard.dart';
|
||||
import 'same_file.dart';
|
||||
|
||||
// 약속 (나누기 file_ops): 로컬 (같은 기기의 폴더 · 네트워크 공유) 끼리의 복사 · 이동 · 맞추기 · 지우기 · 크기.
|
||||
// 입력: 설정 · 상태 [TransferControl], 원본 경로들 · 받는 폴더. 출력: 만든 경로들 · 복사한 파일 수.
|
||||
|
|
@ -129,7 +130,7 @@ Future<void> _deleteExcept(String dir, Set<String> keep) async {
|
|||
Future<bool> isNewerThere(String src, String target) async {
|
||||
final t = File(target);
|
||||
if (!await t.exists()) return false;
|
||||
return (await t.lastModified()).isAfter((await File(src).lastModified()).add(const Duration(seconds: 2)));
|
||||
return SameFile.targetNewer(await t.lastModified(), await File(src).lastModified());
|
||||
}
|
||||
|
||||
/// 229: 같은 이름 창 - 폴더를 합치면 안쪽에서 받는 쪽이 더 새 파일 수 ([limit] 개까지 셈, 로컬만)
|
||||
|
|
@ -224,14 +225,12 @@ Future<int> localMirror(TransferControl c, String srcDir, String dstDir, {bool d
|
|||
final st = await e.stat();
|
||||
final t = File(target);
|
||||
final exists = await t.exists();
|
||||
final same = exists &&
|
||||
(await t.length()) == st.size &&
|
||||
((await t.lastModified()).difference(st.modified).inSeconds.abs() <= 2);
|
||||
if (same) {
|
||||
// 같은 파일 판단은 SameFile 하나 (로컬끼리는 크기 + 바뀐 때 ±2초)
|
||||
if (exists && const SameFile().same(size: st.size, modified: st.modified, targetSize: await t.length(), targetModified: await t.lastModified())) {
|
||||
c.onSettled?.call(e.path, st.size, st.modified);
|
||||
continue;
|
||||
}
|
||||
if (update && exists && (await t.lastModified()).isAfter(st.modified.add(const Duration(seconds: 2)))) continue;
|
||||
if (update && exists && SameFile.targetNewer(await t.lastModified(), st.modified)) continue;
|
||||
final tmp = '$target.jjsync';
|
||||
final out = File(tmp).openWrite();
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
/// 약속 (modular_sync F2): 받는 쪽에 이미 있는 파일을 원본과 "같은 파일" 로 치는지 (다시 보내지 않음) 와 받는 쪽이 더 새것인지.
|
||||
/// 앱이 직접 맞추는 길 (로컬 · 원격) 과 실행 전 미리 보기가 모두 이 하나를 부른다 - 예전에는 미리 보기와 실제 원격 맞추기의 판단이 달라
|
||||
/// 원격 받는 쪽이 더 새 파일이 미리 보기에는 "바뀜" 으로 보이고 실제로는 건너뛰었다.
|
||||
/// - 입력: 원본 · 받는 쪽의 크기와 바뀐 때. 출력: 같은지 · 받는 쪽이 더 새것인지. 오류 없음 (순수).
|
||||
/// - 바뀐 때는 [window] (2초) 안이면 같은 때로 본다 (FAT · 원격의 초 단위 시각).
|
||||
/// - [uploadTime]: 받는 쪽 시각이 "올린 때" 인 곳 (원격이 낀 앱 맞추기, 이동 아님) - 크기가 같고 받는 쪽이 더 새것이어도 같다.
|
||||
/// 이동 (원본을 지움) 은 거의 같은 때일 때만 같다고 본다 (내용이 다른데 원본을 지우지 않게, 214).
|
||||
library;
|
||||
|
||||
class SameFile {
|
||||
const SameFile({this.uploadTime = false});
|
||||
|
||||
/// 앱이 직접 맞출 때의 판단: 원격이 끼고 이동이 아니면 [uploadTime]
|
||||
factory SameFile.forAppSync({required bool remote, required bool move}) => SameFile(uploadTime: remote && !move);
|
||||
|
||||
static const window = Duration(seconds: 2);
|
||||
final bool uploadTime;
|
||||
|
||||
bool same({required int size, required DateTime modified, required int targetSize, required DateTime targetModified}) {
|
||||
if (size != targetSize) return false;
|
||||
if (targetModified.difference(modified).abs() <= window) return true;
|
||||
return uploadTime && !targetModified.isBefore(modified.subtract(window));
|
||||
}
|
||||
|
||||
/// 받는 쪽이 [window] 넘게 더 새것인지 (-u 건너뛰기 · 미리 보기 "더 새 것" 표시)
|
||||
static bool targetNewer(DateTime targetModified, DateTime modified) => targetModified.isAfter(modified.add(window));
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import 'sync_tools.dart' show splitOptions, isSourceRemovingOption;
|
|||
|
||||
export 'sync_tools.dart' show isSourceRemovingOption;
|
||||
import 'vfs.dart';
|
||||
import 'files/same_file.dart';
|
||||
|
||||
/// rsync (Rsync 화면의 → · ← · ⇄) 를 실행하기 전에 무엇이 바뀌는지 미리 보기 (72).
|
||||
enum PreviewAction {
|
||||
|
|
@ -112,6 +113,7 @@ bool optionsUpdate(String options) =>
|
|||
/// [limit] 개를 넘으면 멈춘다 (화면에는 "… 이상").
|
||||
/// 231: 원본의 링크 · junction 은 안으로 들어가지 않는다. [skipLinks] (앱이 직접 - 현재 방식 · WebDAV) 면 "건너뜀 (링크)",
|
||||
/// 아니면 (rsync -a · robocopy) 링크 자체 하나로 센다.
|
||||
/// F2: [same] = 같은 파일 판단 - 실제로 맞출 길과 같은 것 (앱이 직접 맞추고 원격이 끼면 [SameFile.forAppSync], rsync · 로컬은 처음 값).
|
||||
/// 177: [filter] = rsync 옵션의 거르기 규칙 (rsync 로 맞출 때만 - 앱이 직접 맞출 때는 따르지 않으므로 넘기지 않는다).
|
||||
/// 거른 것은 목록에 없고 (거른 폴더 안으로도 내려가지 않음), 받는 쪽의 거른 것은 지우지 않는다 (--delete-excluded 면 지움)
|
||||
Future<List<PreviewItem>> previewSync(String src, String dst,
|
||||
|
|
@ -120,6 +122,7 @@ Future<List<PreviewItem>> previewSync(String src, String dst,
|
|||
bool update = false,
|
||||
bool skipLinks = false,
|
||||
RsyncFilter filter = RsyncFilter.none,
|
||||
SameFile same = const SameFile(),
|
||||
int limit = 2000}) async {
|
||||
final out = <PreviewItem>[];
|
||||
// 177: 받는 쪽에만 있는 폴더 [dir] (상대 경로 [rel]) 안에서 거르지 않은 것만 지울 목록에 (거른 것이 든 폴더는 남김)
|
||||
|
|
@ -196,9 +199,9 @@ Future<List<PreviewItem>> previewSync(String src, String dst,
|
|||
}
|
||||
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) {
|
||||
} else if (!same.same(size: e.size, modified: e.modified, targetSize: t.size, targetModified: t.modified)) {
|
||||
// -u: 받는 쪽이 더 새 것이면 건너뜀. 229: 건너뛰는 것도 · 더 새 것을 덮어쓰는 것도 목록에 따로
|
||||
final newer = t.modified.isAfter(e.modified.add(const Duration(seconds: 2)));
|
||||
final newer = SameFile.targetNewer(t.modified, e.modified);
|
||||
if (update && newer) {
|
||||
out.add(PreviewItem(r, PreviewAction.skipNewer, toRight: toRight, size: e.size));
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import '../core/vfs.dart';
|
|||
import '../l10n/tr.dart';
|
||||
import 'path_label.dart';
|
||||
import 'theme.dart';
|
||||
import '../core/files/same_file.dart';
|
||||
|
||||
/// rsync 실행 전 비교 (72): 파일별로 → · ← · 받는 쪽에만 있음 · 지워짐 (빨강). 창이 열리면 바로 센다.
|
||||
/// [onResult]: 다 세면 결과 (실행 버튼에 지워질 수를 보여 주려고)
|
||||
|
|
@ -266,6 +267,8 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM
|
|||
for (final s in t.sources) ...await previewSync(s, dstOf(s), toRight: true, delete: deletes, update: update,
|
||||
// robocopy 도 링크를 따라가지 않는다 (/XJ - 옵션 칸에 /SJ · /SL 이 없으면)
|
||||
skipLinks: direct || (robo && robocopySkipsLinks(splitOptions(t.options))),
|
||||
// F2: 앱이 직접 맞출 때는 실제 맞추기와 같은 판단 (원격이 끼고 이동이 아니면 받는 쪽이 더 새것도 같음)
|
||||
same: direct ? SameFile.forAppSync(remote: isDav(s) || isDav(dstOf(s)), move: t.move) : const SameFile(),
|
||||
filter: filter),
|
||||
],
|
||||
onStart: () => set(() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/core/files/any_transfer.dart';
|
||||
import 'package:jj_mkvmaker/core/files/local_transfer.dart';
|
||||
import 'package:jj_mkvmaker/core/files/same_file.dart';
|
||||
import 'package:jj_mkvmaker/core/files/transfer_control.dart';
|
||||
import 'package:jj_mkvmaker/core/sync_preview.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// modular_sync F2 약속: "같은 파일" 판단은 SameFile 하나 - 실행 전 미리 보기와 실제 맞추기가 같은 결과를 낸다 (실제 파일 · 실제 맞추기)
|
||||
void main() {
|
||||
final t0 = DateTime(2026, 10, 1, 12);
|
||||
|
||||
test('판단 표: 크기 · 2초 안 · 받는 쪽이 더 새것 (올린 때 · 이동)', () {
|
||||
bool same(SameFile s, {int size = 5, Duration dt = Duration.zero}) =>
|
||||
s.same(size: 5, modified: t0, targetSize: size, targetModified: t0.add(dt));
|
||||
const exact = SameFile();
|
||||
const upload = SameFile(uploadTime: true);
|
||||
expect([same(exact, size: 6), same(upload, size: 6)], [false, false], reason: '크기가 다르면 다름');
|
||||
expect([same(exact, dt: const Duration(seconds: 2)), same(exact, dt: const Duration(seconds: -2))], [true, true]);
|
||||
expect([same(exact, dt: const Duration(minutes: 1)), same(upload, dt: const Duration(minutes: 1))], [false, true], reason: '받는 쪽이 더 새것');
|
||||
expect([same(exact, dt: const Duration(minutes: -1)), same(upload, dt: const Duration(minutes: -1))], [false, false], reason: '받는 쪽이 더 예전');
|
||||
expect([SameFile.forAppSync(remote: true, move: false).uploadTime, SameFile.forAppSync(remote: true, move: true).uploadTime,
|
||||
SameFile.forAppSync(remote: false, move: false).uploadTime], [true, false, false]);
|
||||
expect([SameFile.targetNewer(t0.add(const Duration(seconds: 3)), t0), SameFile.targetNewer(t0.add(const Duration(seconds: 2)), t0)], [true, false]);
|
||||
});
|
||||
|
||||
group('미리 보기 = 실제 맞추기 (받는 쪽 같은 크기 · 1분 더 새것)', () {
|
||||
late Directory tmp;
|
||||
late String s, m;
|
||||
setUp(() {
|
||||
tmp = Directory.systemTemp.createTempSync('jj_samefile_');
|
||||
s = p.join(tmp.path, 's');
|
||||
m = p.join(tmp.path, 'm');
|
||||
File(p.join(s, 'a.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('SRC')
|
||||
..setLastModifiedSync(t0);
|
||||
File(p.join(m, 'a.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('DST')
|
||||
..setLastModifiedSync(t0.add(const Duration(minutes: 1)));
|
||||
});
|
||||
tearDown(() => tmp.deleteSync(recursive: true));
|
||||
String there() => File(p.join(m, 'a.txt')).readAsStringSync();
|
||||
|
||||
test('원격이 낀 앱 맞추기: 미리 보기에 없고 · 실제로도 보내지 않음', () async {
|
||||
final items = await previewSync(s, m, toRight: true, same: SameFile.forAppSync(remote: true, move: false));
|
||||
expect(items, isEmpty);
|
||||
expect(await anyMirror(TransferControl(), s, m), 0);
|
||||
expect(there(), 'DST');
|
||||
});
|
||||
|
||||
test('이동 (원본을 지움): 미리 보기에 "바뀜 (더 새 것)" · 실제로도 보냄', () async {
|
||||
final items = await previewSync(s, m, toRight: true, same: SameFile.forAppSync(remote: true, move: true));
|
||||
expect([items.single.action, items.single.newer], [PreviewAction.update, true]);
|
||||
expect(await anyMirror(TransferControl(), s, m, strict: true), 1);
|
||||
expect(there(), 'SRC');
|
||||
});
|
||||
|
||||
test('로컬끼리: 미리 보기에 "바뀜 (더 새 것)" · 실제로도 보냄', () async {
|
||||
final items = await previewSync(s, m, toRight: true);
|
||||
expect([items.single.action, items.single.newer], [PreviewAction.update, true]);
|
||||
expect(await localMirror(TransferControl(), s, m), 1);
|
||||
expect(there(), 'SRC');
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue