JJ_MKVMaker/lib/app/sync/sync_diff.dart

171 lines
8.1 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/// 약속: 실시간 동기화 쌍의 "맞출 것" 계산 (rsync 가 할 일을 미리 보기). [SyncDiff.of] 는 보낼 것 (상대 경로) 과 지울 것 ("− 경로")
/// 을 [limit] 개까지 준다 - 옵션의 거르기 ([RsyncFilter]) 를 따르고, 받는 쪽에서 지키는 것은 지우지 않는다. 한쪽이라도 dav:// 면 WebDAV 판.
/// 파일 · 폴더를 읽기만 한다 (바꾸지 않음). 원본을 읽지 못한 폴더는 세지 않는다.
library;
import 'dart:io';
import 'package:path/path.dart' as p;
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 와 같게):
/// 거른 것은 지우지 않고 (--delete-excluded 면 지움), 안에 거른 것이 든 폴더는 남기고 그 안의 거르지 않은 것만.
/// 10/20 (15, 28 의 장면으로 찾음): 예전에는 그런 폴더도 "− 폴더" 한 줄이라 지울지 묻는 창에 폴더가 통째로 보이고,
/// 맞춘 뒤에도 (폴더가 남으므로) 맞출 것에 계속 남았다.
/// [excluded] 가 있으면 --delete-excluded 때문에 지워지는 것 (거른 것 · 거른 것이 든 폴더) 을 거기에도 (확인 창에서 따로 묶음)
static Future<void> _gone(String path, String r, bool isDir, RsyncFilter filter, List<String> out, int limit,
[List<String>? excluded]) async {
if (out.length >= limit) return;
// 관리자 지시 (10/20): 받는 쪽 규칙 (- · P · R · ,r - --delete-excluded 면 쪽을 적지 않은 규칙은 빠짐) 에 걸린 것만 지킨다
if (filter.protects(r, isDir: isDir)) return;
Future<void> mark() async {
// --delete-excluded 때문에 지워지는 것: 보내는 쪽에서 거른 것 · 그런 것이 든 폴더
if (excluded != null &&
filter.deleteExcluded &&
(filter.excluded(r, isDir: isDir) || (isDir && await _hasExcluded(path, r, filter, sender: true)))) {
excluded.add(r);
}
}
if (!isDir || !filter.protectsAny || !await _hasExcluded(path, r, filter)) {
out.add('− $r');
await mark();
return;
}
List<({String path, bool isDir, int size, DateTime modified})> items;
try {
items = await vList(path);
} catch (_) {
return;
}
for (final e in items) {
final name = vBasename(e.path);
if (name.endsWith('.jjsync') || isAppTempName(name)) continue;
await _gone(e.path, '$r/$name', e.isDir, filter, out, limit, excluded);
}
}
/// 폴더 [dir] (상대 경로 [rel]) 안에 받는 쪽에서 지키는 것 ([sender] 면 보내는 쪽에서 거른 것) 이 하나라도 있는지
static Future<bool> _hasExcluded(String dir, String rel, RsyncFilter filter, {bool sender = false}) async {
List<({String path, bool isDir, int size, DateTime modified})> items;
try {
items = await vList(dir);
} catch (_) {
return false;
}
for (final e in items) {
final r = '$rel/${vBasename(e.path)}';
if (sender ? filter.excluded(r, isDir: e.isDir) : filter.protects(r, isDir: e.isDir)) return true;
if (e.isDir && await _hasExcluded(e.path, r, filter, sender: sender)) return true;
}
return false;
}
/// [excluded]: 지울 것 가운데 --delete-excluded 때문에 지워지는 것 (거른 것 · 거른 것이 든 폴더) 을 모은다 ([_gone])
static Future<List<String>> of(LiveSyncPair x,
{int limit = 500, RsyncFilter filter = RsyncFilter.none, List<String>? excluded}) async {
if (isDav(x.source) || isDav(x.target)) return _ofDav(x, limit: limit, filter: filter, excluded: excluded);
final out = <String>[];
Future<void> walk(String src, String dst, String rel) async {
if (out.length >= limit) return;
final names = <String>{};
List<FileSystemEntity> items;
try {
items = await Directory(src).list(followLinks: false).toList();
} catch (_) {
return; // 원본을 읽지 못함: 이 폴더의 "지울 것" 은 세지 않는다
}
for (final e in items) {
if (out.length >= limit) return;
final name = p.basename(e.path);
if (isAppTempName(name)) continue; // 78 보충: 이름 바꾸는 중의 임시 이름은 맞출 것이 아니다
final r = rel.isEmpty ? name : '$rel/$name';
// 10/20 (15, 28 의 장면으로 찾음): rsync 쌍은 옵션의 거르기를 따른다 - 예전에는 rsync 가 보내지 않는 것을
// 맞출 것으로 늘 세고, 지우지 않는 것 (받는 쪽의 거른 것) 을 지울지 물었다
if (filter.excluded(r, isDir: e is Directory)) continue;
names.add(name);
final t = p.join(dst, name);
if (e is Directory) {
await walk(e.path, t, r);
} else if (e is File) {
final st = await e.stat();
final tf = File(t);
if (!await tf.exists()) {
out.add(r);
continue;
}
final ts = await tf.stat();
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()) {
await for (final e in Directory(dst).list(followLinks: false).handleError((_) {})) {
final name = p.basename(e.path);
if (names.contains(name) || name.endsWith('.jjsync') || isAppTempName(name)) continue;
await _gone(e.path, rel.isEmpty ? name : '$rel/$name', e is Directory, filter, out, limit, excluded);
}
}
}
if (await Directory(x.source).exists()) await walk(x.source, x.target, '');
return out;
}
/// [of] 의 WebDAV 판 (한쪽이라도 dav://): 크기가 다르거나, 대상이 원본보다 옛것이면
static Future<List<String>> _ofDav(LiveSyncPair x,
{int limit = 500, RsyncFilter filter = RsyncFilter.none, List<String>? excluded}) async {
final out = <String>[];
Future<void> walk(String src, String dst, String rel) async {
if (out.length >= limit) return;
final there = <String, ({String path, bool isDir, int size, DateTime modified})>{};
try {
for (final e in await vList(dst)) {
there[vBasename(e.path)] = e;
}
} catch (_) {} // 대상 폴더가 아직 없음
final names = <String>{};
List<({String path, bool isDir, int size, DateTime modified})> items;
try {
items = await vList(src, strict: true);
} catch (_) {
return; // 원본을 읽지 못함: "지울 것" 을 세지 않는다
}
for (final e in items) {
if (out.length >= limit) return;
final name = vBasename(e.path);
if (isAppTempName(name)) continue; // 78 보충
final r = rel.isEmpty ? name : '$rel/$name';
if (filter.excluded(r, isDir: e.isDir)) continue; // 관리자 지시 (10/19): 맞추기 (_mirrorV) 와 같은 거르기
names.add(name);
if (e.isDir) {
await walk(e.path, vJoin(dst, name), r);
continue;
}
final t = there[name];
// M1: 받는 쪽이 같은 이름의 폴더면 지우기 포함일 때만 바뀐다 (아니면 건너뜀 - 기다리는 것으로 세지 않음)
if (t != null && t.isDir && !x.delete) continue;
// 원격 맞추기와 같은 판단 (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);
}
}
if (x.delete) {
for (final t in there.entries) {
final name = t.key;
if (names.contains(name) || name.endsWith('.jjsync') || isAppTempName(name)) continue;
await _gone(t.value.path, rel.isEmpty ? name : '$rel/$name', t.value.isDir, filter, out, limit, excluded);
}
}
}
if (await vExists(x.source)) await walk(x.source, x.target, '');
return out;
}
}