modular_sync F1: 맞추기는 로컬 · 원격 모두 같은 구현 하나 (anyMirror) - 로컬도 거르기 · 같은 이름 폴더 충돌을 따름
- FileOps.mirror 가 늘 anyMirror 를 부름. localMirror (거르기를 무시 · 같은 이름 폴더 충돌 (M1) 을 모름) 를 지움 - anyMirror 의 같은 파일 판단: SameFile.forAppSync(remote: 경로에 원격이 있는지, move: strict) - "받는 쪽이 더 새것도 같음" 은 원격일 때만 (로컬끼리는 크기 + ±2초 그대로) - 약속 시험: mirror_one_impl (실제 폴더 - 거르기: 거른 것은 보내지 않고 받는 쪽의 거른 것은 지우지 않음 · 나머지는 --delete, 미리 보기와 같음 / 같은 이름 폴더: 지우기 없으면 건너뛰고 알림 · 있으면 파일로). same_file 의 원격 경우는 실제 WebDAV 시험 서버로 사용자가 보게 될 동작: 로컬끼리 맞추기 (현재 방식 · 실시간 동기화) 가 rsync 옵션의 거르기를 따름 - 미리 보기에서 빠진 것은 실제로도 보내지 않고 받는 쪽의 거른 것은 지우지 않음 (예전에는 미리 보기와 달리 다 보내고 다 지웠다). 받는 쪽의 같은 이름 폴더는 지우기 포함일 때만 바꿈 확인: analyze 0 · 단위 1484 ×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
9d1883311f
commit
81a2c169ff
|
|
@ -75,8 +75,8 @@ class FileOps extends TransferControl {
|
|||
{bool delete = false, bool update = false, bool allowEmptySource = false, bool strict = false, RsyncFilter filter = RsyncFilter.none}) async {
|
||||
// 지우기 포함이면 원본을 확실히 읽을 수 있을 때만 (읽지 못하면 원본이 빈 것으로 보여 대상이 모두 지워졌다)
|
||||
if (delete) await ensureSourceForDelete(srcDir, dstDir, allowEmpty: allowEmptySource);
|
||||
if (_anyDav([srcDir, dstDir])) return anyMirror(this, srcDir, dstDir, delete: delete, update: update, strict: strict, filter: filter);
|
||||
return local.localMirror(this, srcDir, dstDir, delete: delete, update: update);
|
||||
// F1 (modular_sync): 로컬 · 원격 모두 같은 구현 하나 - 예전 로컬 맞추기는 거르기 (filter) 를 무시하고 같은 이름 폴더 충돌 (M1) 도 몰랐다
|
||||
return anyMirror(this, srcDir, dstDir, delete: delete, update: update, strict: strict, filter: filter);
|
||||
}
|
||||
|
||||
Future<void> delete(List<String> paths) => local.deletePaths(this, paths);
|
||||
|
|
|
|||
|
|
@ -171,7 +171,8 @@ 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);
|
||||
// 받는 쪽 시각이 올린 때인 곳은 원격이 낄 때만 (로컬끼리는 크기 + ±2초) - 미리 보기와 같은 판단 (F1 · F2)
|
||||
final same = SameFile.forAppSync(remote: isDav(srcDir) || isDav(dstDir), move: strict);
|
||||
// 관리자 지시 (10/19): 거르기는 rsync 와 같게 (미리 보기 sync_preview 와 같은 규칙) - 거른 것은 보내지 않고 받는 쪽의 거른 것은 지우지 않는다
|
||||
// (--delete-excluded 면 지운다). 받는 쪽에만 있는 폴더 안에 거른 것이 있으면 폴더는 남기고 거르지 않은 것만 지운다
|
||||
Future<bool> hasExcluded(String dir, String rel) async {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import '../../l10n/tr.dart';
|
|||
import '../vfs.dart';
|
||||
import 'path_rules.dart';
|
||||
import 'transfer_control.dart';
|
||||
import 'transfer_guard.dart';
|
||||
import 'same_file.dart';
|
||||
|
||||
// 약속 (나누기 file_ops): 로컬 (같은 기기의 폴더 · 네트워크 공유) 끼리의 복사 · 이동 · 맞추기 · 지우기 · 크기.
|
||||
|
|
@ -198,73 +197,6 @@ Future<List<String>> localMove(TransferControl c, List<String> sources, String d
|
|||
return made;
|
||||
}
|
||||
|
||||
/// 동기화 (실시간 동기화 · lsyncd 처럼): [srcDir] 의 내용을 [dstDir] 에 맞춘다 (로컬끼리).
|
||||
/// 크기나 바뀐 시각이 다른 파일만 복사, [delete] 면 원본에 없는 것을 대상에서 지운다. 복사한 파일 수를 돌려준다.
|
||||
/// [update] (rsync -u): 대상이 더 새 파일이면 건너뛴다. 지우기 전 원본 확인 (ensureSourceForDelete) 은 부르는 쪽이 한다.
|
||||
Future<int> localMirror(TransferControl c, String srcDir, String dstDir, {bool delete = false, bool update = false}) async {
|
||||
var n = 0;
|
||||
Future<void> walk(String src, String dst) async {
|
||||
c.check();
|
||||
await Directory(dst).create(recursive: true);
|
||||
final names = <String>{};
|
||||
// 원본 목록을 다 읽은 뒤에 맞춘다: 읽다 실패하면 (그 폴더에서) 아무것도 지우지 않고 멈춘다
|
||||
List<FileSystemEntity> items;
|
||||
try {
|
||||
items = await Directory(src).list(followLinks: false).toList();
|
||||
} catch (e) {
|
||||
throw SourceUnreadableException(src, cause: e);
|
||||
}
|
||||
for (final e in items) {
|
||||
final name = p.basename(e.path);
|
||||
if (isAppTempName(name)) continue; // 78 보충: 이름 바꾸는 중의 임시 이름은 맞추지 않는다
|
||||
names.add(name);
|
||||
final target = p.join(dst, name);
|
||||
if (e is Directory) {
|
||||
await walk(e.path, target);
|
||||
} else if (e is File) {
|
||||
final st = await e.stat();
|
||||
final t = File(target);
|
||||
final exists = await t.exists();
|
||||
// 같은 파일 판단은 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 && SameFile.targetNewer(await t.lastModified(), st.modified)) continue;
|
||||
final tmp = '$target.jjsync';
|
||||
final out = File(tmp).openWrite();
|
||||
try {
|
||||
await for (final chunk in e.openRead()) {
|
||||
c.check();
|
||||
out.add(chunk);
|
||||
c.onBytes?.call(chunk.length);
|
||||
await c.throttle(chunk.length);
|
||||
}
|
||||
} finally {
|
||||
await out.close();
|
||||
}
|
||||
await File(tmp).rename(target);
|
||||
try {
|
||||
await File(target).setLastModified(st.modified);
|
||||
} catch (_) {}
|
||||
n++;
|
||||
c.onFileDone?.call(e.path);
|
||||
c.onSettled?.call(e.path, st.size, st.modified);
|
||||
}
|
||||
}
|
||||
if (delete) {
|
||||
await for (final e in Directory(dst).list(followLinks: false).handleError((_) {})) {
|
||||
if (!names.contains(p.basename(e.path)) && !e.path.endsWith('.jjsync') && !isAppTempName(p.basename(e.path))) {
|
||||
await e.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(srcDir, dstDir);
|
||||
return n;
|
||||
}
|
||||
|
||||
/// 지우기 (로컬 · WebDAV). 없는 것은 그냥 지나간다. 항목마다 취소를 본다
|
||||
Future<void> deletePaths(TransferControl c, List<String> paths) async {
|
||||
for (final s in paths) {
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ void main() {
|
|||
expect(File(moved.single).readAsStringSync(), 'hello');
|
||||
final m = p.join(tmp.path, 'm');
|
||||
mk('m/old.txt');
|
||||
expect(await localMirror(TransferControl(), p.join(tmp.path, 'd'), m, delete: true), 2);
|
||||
expect(await anyMirror(TransferControl(), p.join(tmp.path, 'd'), m, delete: true), 2, reason: 'F1: 로컬 맞추기도 anyMirror 하나');
|
||||
expect(File(p.join(m, 'old.txt')).existsSync(), isFalse);
|
||||
final c = TransferControl()..cancel();
|
||||
await expectLater(localCopy(c, [moved.single], p.join(tmp.path, 's')), throwsA(isA<FileOpCancelled>()));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/core/file_ops.dart' show FileOps;
|
||||
import 'package:jj_mkvmaker/core/rsync_filter.dart';
|
||||
import 'package:jj_mkvmaker/core/sync_preview.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// modular_sync F1 약속: 맞추기는 로컬 · 원격 모두 같은 구현 하나 (anyMirror) - 예전 로컬 맞추기는 거르기 (filter) 를 무시하고
|
||||
/// 받는 쪽의 같은 이름 폴더 충돌도 몰랐다. 실제 시험 폴더 · 실제 맞추기, 실행 전 미리 보기와 같은 결과
|
||||
void main() {
|
||||
late Directory tmp;
|
||||
late String s, m;
|
||||
setUp(() {
|
||||
tmp = Directory.systemTemp.createTempSync('jj_mirror1_');
|
||||
s = p.join(tmp.path, 's');
|
||||
m = p.join(tmp.path, 'm');
|
||||
});
|
||||
tearDown(() => tmp.deleteSync(recursive: true));
|
||||
void mk(String path, [String text = 'x']) => File(path)
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(text);
|
||||
bool there(String rel) => FileSystemEntity.typeSync(p.join(m, rel)) != FileSystemEntityType.notFound;
|
||||
|
||||
test('로컬도 거르기를 따른다: 거른 것은 보내지 않고 · 받는 쪽의 거른 것은 지우지 않음 · 나머지는 --delete 로 지움 (미리 보기와 같음)', () async {
|
||||
mk(p.join(s, 'keep.txt'));
|
||||
mk(p.join(s, 'skip.tmp'));
|
||||
mk(p.join(s, 'cache', 'x.txt'));
|
||||
mk(p.join(m, 'old.tmp'));
|
||||
mk(p.join(m, 'extra.txt'));
|
||||
final filter = RsyncFilter.parse('--exclude=*.tmp --exclude=cache/');
|
||||
final preview = await previewSync(s, m, toRight: true, delete: true, filter: filter);
|
||||
final adds = {for (final i in preview) if (i.action == PreviewAction.add) i.rel};
|
||||
final dels = {for (final i in preview) if (i.action == PreviewAction.delete) i.rel};
|
||||
expect([adds, dels], [{'keep.txt'}, {'extra.txt'}]);
|
||||
await FileOps().mirror(s, m, delete: true, filter: filter);
|
||||
expect([there('keep.txt'), there('skip.tmp'), there('cache'), there('old.tmp'), there('extra.txt')], [true, false, false, true, false]);
|
||||
});
|
||||
|
||||
test('받는 쪽에 같은 이름의 폴더: 지우기 없으면 건너뛰고 알림 · 지우기 포함이면 폴더를 지우고 파일로', () async {
|
||||
mk(p.join(s, 'clash'), 'file');
|
||||
mk(p.join(m, 'clash', 'inner.txt'));
|
||||
final ops = FileOps();
|
||||
await ops.mirror(s, m);
|
||||
expect([FileSystemEntity.isDirectorySync(p.join(m, 'clash')), ops.dirConflicts], [true, [p.join(m, 'clash')]]);
|
||||
await FileOps().mirror(s, m, delete: true);
|
||||
expect(File(p.join(m, 'clash')).readAsStringSync(), 'file');
|
||||
});
|
||||
}
|
||||
|
|
@ -2,11 +2,13 @@ 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;
|
||||
import 'package:jj_mkvmaker/core/webdav.dart';
|
||||
|
||||
import 'support/dav_server.dart';
|
||||
|
||||
/// modular_sync F2 약속: "같은 파일" 판단은 SameFile 하나 - 실행 전 미리 보기와 실제 맞추기가 같은 결과를 낸다 (실제 파일 · 실제 맞추기)
|
||||
void main() {
|
||||
|
|
@ -29,7 +31,9 @@ void main() {
|
|||
group('미리 보기 = 실제 맞추기 (받는 쪽 같은 크기 · 1분 더 새것)', () {
|
||||
late Directory tmp;
|
||||
late String s, m;
|
||||
setUp(() {
|
||||
late TestDavServer server;
|
||||
// 받는 쪽: 로컬 m 과, 실제 WebDAV 시험 서버 (dav://nas/m) 에 같은 파일
|
||||
setUp(() async {
|
||||
tmp = Directory.systemTemp.createTempSync('jj_samefile_');
|
||||
s = p.join(tmp.path, 's');
|
||||
m = p.join(tmp.path, 'm');
|
||||
|
|
@ -37,33 +41,44 @@ void main() {
|
|||
..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)));
|
||||
for (final dir in [m, p.join(tmp.path, 'remote', 'm')]) {
|
||||
File(p.join(dir, 'a.txt'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('DST')
|
||||
..setLastModifiedSync(t0.add(const Duration(minutes: 1)));
|
||||
}
|
||||
server = TestDavServer(Directory(p.join(tmp.path, 'remote')));
|
||||
await server.start();
|
||||
DavRegistry.configure([DavServer(id: 'nas', name: 'NAS', url: server.url, user: 'user', password: 'pass')]);
|
||||
});
|
||||
tearDown(() => tmp.deleteSync(recursive: true));
|
||||
String there() => File(p.join(m, 'a.txt')).readAsStringSync();
|
||||
tearDown(() async {
|
||||
DavRegistry.configure([]);
|
||||
await server.stop();
|
||||
tmp.deleteSync(recursive: true);
|
||||
});
|
||||
String localThere() => File(p.join(m, 'a.txt')).readAsStringSync();
|
||||
String remoteThere() => File(p.join(tmp.path, 'remote', 'm', 'a.txt')).readAsStringSync();
|
||||
const remote = 'dav://nas/m';
|
||||
|
||||
test('원격이 낀 앱 맞추기: 미리 보기에 없고 · 실제로도 보내지 않음', () async {
|
||||
final items = await previewSync(s, m, toRight: true, same: SameFile.forAppSync(remote: true, move: false));
|
||||
final items = await previewSync(s, remote, toRight: true, same: SameFile.forAppSync(remote: true, move: false));
|
||||
expect(items, isEmpty);
|
||||
expect(await anyMirror(TransferControl(), s, m), 0);
|
||||
expect(there(), 'DST');
|
||||
expect(await anyMirror(TransferControl(), s, remote), 0);
|
||||
expect(remoteThere(), 'DST');
|
||||
});
|
||||
|
||||
test('이동 (원본을 지움): 미리 보기에 "바뀜 (더 새 것)" · 실제로도 보냄', () async {
|
||||
final items = await previewSync(s, m, toRight: true, same: SameFile.forAppSync(remote: true, move: true));
|
||||
test('원격 이동 (원본을 지움): 미리 보기에 "바뀜 (더 새 것)" · 실제로도 보냄', () async {
|
||||
final items = await previewSync(s, remote, 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');
|
||||
expect(await anyMirror(TransferControl(), s, remote, strict: true), 1);
|
||||
expect(remoteThere(), 'SRC');
|
||||
});
|
||||
|
||||
test('로컬끼리: 미리 보기에 "바뀜 (더 새 것)" · 실제로도 보냄', () async {
|
||||
final items = await previewSync(s, m, toRight: true);
|
||||
test('로컬끼리: 미리 보기에 "바뀜 (더 새 것)" · 실제로도 보냄 (F1: 로컬도 같은 구현)', () async {
|
||||
final items = await previewSync(s, m, toRight: true, same: SameFile.forAppSync(remote: false, move: false));
|
||||
expect([items.single.action, items.single.newer], [PreviewAction.update, true]);
|
||||
expect(await localMirror(TransferControl(), s, m), 1);
|
||||
expect(there(), 'SRC');
|
||||
expect(await anyMirror(TransferControl(), s, m), 1);
|
||||
expect(localThere(), 'SRC');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue