171 lines
8.1 KiB
Dart
171 lines
8.1 KiB
Dart
/// 약속: 실시간 동기화 쌍의 "맞출 것" 계산 (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;
|
||
}
|
||
}
|