fix: cloud sync (#769)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-06-04 00:11:31 +08:00
committed by GitHub
parent 9547d92ac5
commit 0c1ada0067
70 changed files with 2348 additions and 1906 deletions

View File

@@ -0,0 +1,237 @@
import 'dart:convert';
import 'dart:io';
import 'package:fl_lib/fl_lib.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:logging/logging.dart';
import 'package:server_box/data/model/server/private_key_info.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/snippet.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/store.dart';
part 'backup.g.dart';
const backupFormatVersion = 1;
final _logger = Logger('Backup');
@JsonSerializable()
class Backup implements Mergeable {
// backup format version
final int version;
final String date;
final List<Spi> spis;
final List<Snippet> snippets;
final List<PrivateKeyInfo> keys;
final Map<String, dynamic> container;
final Map<String, dynamic> history;
final int? lastModTime;
final Map<String, dynamic>? settings;
const Backup({
required this.version,
required this.date,
required this.spis,
required this.snippets,
required this.keys,
required this.container,
required this.history,
required this.settings,
this.lastModTime,
});
factory Backup.fromJson(Map<String, dynamic> json) => _$BackupFromJson(json);
Map<String, dynamic> toJson() => _$BackupToJson(this);
static Future<Backup> loadFromStore() async {
final lastModTime = Stores.lastModTime;
return Backup(
version: backupFormatVersion,
date: DateTime.now().toString().split('.').firstOrNull ?? '',
spis: Stores.server.fetch(),
snippets: Stores.snippet.fetch(),
keys: Stores.key.fetch(),
container: Stores.container.getAllMap(),
lastModTime: lastModTime,
history: Stores.history.getAllMap(),
settings: Stores.setting.getAllMap(),
);
}
static Future<String> backup([String? name]) async {
final bak = await Backup.loadFromStore();
final result = _diyEncrypt(json.encode(bak.toJson()));
final path = Paths.doc.joinPath(name ?? Miscs.bakFileName);
await File(path).writeAsString(result);
return path;
}
@override
Future<void> merge({bool force = false}) async {
final curTime = Stores.lastModTime;
final bakTime = lastModTime ?? 0;
final shouldRestore = force || curTime < bakTime;
if (!shouldRestore) {
_logger.info('No need to restore, local is newer');
return;
}
// Snippets
if (force) {
for (final s in snippets) {
Stores.snippet.box.put(s.name, s);
}
} else {
final nowSnippets = Stores.snippet.box.keys.toSet();
final bakSnippets = snippets.map((e) => e.name).toSet();
final newSnippets = bakSnippets.difference(nowSnippets);
final delSnippets = nowSnippets.difference(bakSnippets);
final updateSnippets = nowSnippets.intersection(bakSnippets);
for (final s in newSnippets) {
Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s));
}
for (final s in delSnippets) {
Stores.snippet.box.delete(s);
}
for (final s in updateSnippets) {
Stores.snippet.box.put(s, snippets.firstWhere((e) => e.name == s));
}
}
// ServerPrivateInfo
if (force) {
for (final s in spis) {
Stores.server.box.put(s.id, s);
}
} else {
final nowSpis = Stores.server.box.keys.toSet();
final bakSpis = spis.map((e) => e.id).toSet();
final newSpis = bakSpis.difference(nowSpis);
final delSpis = nowSpis.difference(bakSpis);
final updateSpis = nowSpis.intersection(bakSpis);
for (final s in newSpis) {
Stores.server.box.put(s, spis.firstWhere((e) => e.id == s));
}
for (final s in delSpis) {
Stores.server.box.delete(s);
}
for (final s in updateSpis) {
Stores.server.box.put(s, spis.firstWhere((e) => e.id == s));
}
}
// PrivateKeyInfo
if (force) {
for (final s in keys) {
Stores.key.box.put(s.id, s);
}
} else {
final nowKeys = Stores.key.box.keys.toSet();
final bakKeys = keys.map((e) => e.id).toSet();
final newKeys = bakKeys.difference(nowKeys);
final delKeys = nowKeys.difference(bakKeys);
final updateKeys = nowKeys.intersection(bakKeys);
for (final s in newKeys) {
Stores.key.box.put(s, keys.firstWhere((e) => e.id == s));
}
for (final s in delKeys) {
Stores.key.box.delete(s);
}
for (final s in updateKeys) {
Stores.key.box.put(s, keys.firstWhere((e) => e.id == s));
}
}
// History
if (force) {
Stores.history.box.putAll(history);
} else {
final nowHistory = Stores.history.box.keys.toSet();
final bakHistory = history.keys.toSet();
final newHistory = bakHistory.difference(nowHistory);
final delHistory = nowHistory.difference(bakHistory);
final updateHistory = nowHistory.intersection(bakHistory);
for (final s in newHistory) {
Stores.history.box.put(s, history[s]);
}
for (final s in delHistory) {
Stores.history.box.delete(s);
}
for (final s in updateHistory) {
Stores.history.box.put(s, history[s]);
}
}
// Container
if (force) {
Stores.container.box.putAll(container);
} else {
final nowContainer = Stores.container.box.keys.toSet();
final bakContainer = container.keys.toSet();
final newContainer = bakContainer.difference(nowContainer);
final delContainer = nowContainer.difference(bakContainer);
final updateContainer = nowContainer.intersection(bakContainer);
for (final s in newContainer) {
Stores.container.box.put(s, container[s]);
}
for (final s in delContainer) {
Stores.container.box.delete(s);
}
for (final s in updateContainer) {
Stores.container.box.put(s, container[s]);
}
}
// Settings
final settings_ = settings;
if (settings_ != null) {
if (force) {
Stores.setting.box.putAll(settings_);
} else {
final nowSettings = Stores.setting.box.keys.toSet();
final bakSettings = settings_.keys.toSet();
final newSettings = bakSettings.difference(nowSettings);
final delSettings = nowSettings.difference(bakSettings);
final updateSettings = nowSettings.intersection(bakSettings);
for (final s in newSettings) {
Stores.setting.box.put(s, settings_[s]);
}
for (final s in delSettings) {
Stores.setting.box.delete(s);
}
for (final s in updateSettings) {
Stores.setting.box.put(s, settings_[s]);
}
}
}
Provider.reload();
RNodes.app.notify();
_logger.info('Restore success');
}
factory Backup.fromJsonString(String raw) =>
Backup.fromJson(json.decode(_diyDecrypt(raw)));
}
String _diyEncrypt(String raw) => json.encode(
raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false),
);
String _diyDecrypt(String raw) {
try {
final list = json.decode(raw);
final sb = StringBuffer();
for (final e in list) {
sb.writeCharCode((e - 1) ~/ 2);
}
return sb.toString();
} catch (e, trace) {
Loggers.app.warning('Backup decrypt failed', e, trace);
rethrow;
}
}

View File

@@ -0,0 +1,37 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'backup.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Backup _$BackupFromJson(Map<String, dynamic> json) => Backup(
version: (json['version'] as num).toInt(),
date: json['date'] as String,
spis: (json['spis'] as List<dynamic>)
.map((e) => Spi.fromJson(e as Map<String, dynamic>))
.toList(),
snippets: (json['snippets'] as List<dynamic>)
.map((e) => Snippet.fromJson(e as Map<String, dynamic>))
.toList(),
keys: (json['keys'] as List<dynamic>)
.map((e) => PrivateKeyInfo.fromJson(e as Map<String, dynamic>))
.toList(),
container: json['container'] as Map<String, dynamic>,
history: json['history'] as Map<String, dynamic>,
settings: json['settings'] as Map<String, dynamic>?,
lastModTime: (json['lastModTime'] as num?)?.toInt(),
);
Map<String, dynamic> _$BackupToJson(Backup instance) => <String, dynamic>{
'version': instance.version,
'date': instance.date,
'spis': instance.spis,
'snippets': instance.snippets,
'keys': instance.keys,
'container': instance.container,
'history': instance.history,
'lastModTime': instance.lastModTime,
'settings': instance.settings,
};

View File

@@ -0,0 +1,89 @@
import 'dart:convert';
import 'dart:io';
import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:logging/logging.dart';
import 'package:server_box/data/res/misc.dart';
import 'package:server_box/data/res/store.dart';
part 'backup2.freezed.dart';
part 'backup2.g.dart';
final _loggerV2 = Logger('BackupV2');
@freezed
abstract class BackupV2 with _$BackupV2 implements Mergeable {
const BackupV2._();
/// Construct a backup with the latest format (v2).
///
/// All `Map<String, dynamic>` are:
/// ```json
/// {
/// "key1": Model{},
/// "_lastModTime": {
/// "key1": 1234567890,
/// },
/// }
/// ```
const factory BackupV2({
required int version,
required int date,
required Map<String, Object?> spis,
required Map<String, Object?> snippets,
required Map<String, Object?> keys,
required Map<String, Object?> container,
required Map<String, Object?> history,
required Map<String, Object?> settings,
}) = _BackupV2;
factory BackupV2.fromJson(Map<String, dynamic> json) => _$BackupV2FromJson(json);
@override
Future<void> merge({bool force = false}) async {
_loggerV2.info('Merging...');
// Merge each store
await Mergeable.mergeStore(backupData: spis, store: Stores.server, force: force);
await Mergeable.mergeStore(backupData: snippets, store: Stores.snippet, force: force);
await Mergeable.mergeStore(backupData: keys, store: Stores.key, force: force);
await Mergeable.mergeStore(backupData: container, store: Stores.container, force: force);
await Mergeable.mergeStore(backupData: history, store: Stores.history, force: force);
await Mergeable.mergeStore(backupData: settings, store: Stores.setting, force: force);
// Reload providers and notify listeners
Provider.reload();
RNodes.app.notify();
_loggerV2.info('Merge completed');
}
static const formatVer = 2;
static Future<BackupV2> loadFromStore() async {
return BackupV2(
version: formatVer,
date: DateTimeX.timestamp,
spis: Stores.server.getAllMap(includeInternalKeys: true),
snippets: Stores.snippet.getAllMap(includeInternalKeys: true),
keys: Stores.key.getAllMap(includeInternalKeys: true),
container: Stores.container.getAllMap(includeInternalKeys: true),
history: Stores.history.getAllMap(includeInternalKeys: true),
settings: Stores.setting.getAllMap(includeInternalKeys: true),
);
}
static Future<String> backup([String? name]) async {
final bak = await BackupV2.loadFromStore();
final result = json.encode(bak.toJson());
final path = Paths.doc.joinPath(name ?? Miscs.bakFileName);
await File(path).writeAsString(result);
return path;
}
factory BackupV2.fromJsonString(String jsonString) {
final map = json.decode(jsonString) as Map<String, dynamic>;
return BackupV2.fromJson(map);
}
}

View File

@@ -0,0 +1,372 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'backup2.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
BackupV2 _$BackupV2FromJson(Map<String, dynamic> json) {
return _BackupV2.fromJson(json);
}
/// @nodoc
mixin _$BackupV2 {
int get version => throw _privateConstructorUsedError;
int get date => throw _privateConstructorUsedError;
Map<String, Object?> get spis => throw _privateConstructorUsedError;
Map<String, Object?> get snippets => throw _privateConstructorUsedError;
Map<String, Object?> get keys => throw _privateConstructorUsedError;
Map<String, Object?> get container => throw _privateConstructorUsedError;
Map<String, Object?> get history => throw _privateConstructorUsedError;
Map<String, Object?> get settings => throw _privateConstructorUsedError;
/// Serializes this BackupV2 to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of BackupV2
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$BackupV2CopyWith<BackupV2> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $BackupV2CopyWith<$Res> {
factory $BackupV2CopyWith(BackupV2 value, $Res Function(BackupV2) then) =
_$BackupV2CopyWithImpl<$Res, BackupV2>;
@useResult
$Res call({
int version,
int date,
Map<String, Object?> spis,
Map<String, Object?> snippets,
Map<String, Object?> keys,
Map<String, Object?> container,
Map<String, Object?> history,
Map<String, Object?> settings,
});
}
/// @nodoc
class _$BackupV2CopyWithImpl<$Res, $Val extends BackupV2>
implements $BackupV2CopyWith<$Res> {
_$BackupV2CopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of BackupV2
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? version = null,
Object? date = null,
Object? spis = null,
Object? snippets = null,
Object? keys = null,
Object? container = null,
Object? history = null,
Object? settings = null,
}) {
return _then(
_value.copyWith(
version: null == version
? _value.version
: version // ignore: cast_nullable_to_non_nullable
as int,
date: null == date
? _value.date
: date // ignore: cast_nullable_to_non_nullable
as int,
spis: null == spis
? _value.spis
: spis // ignore: cast_nullable_to_non_nullable
as Map<String, Object?>,
snippets: null == snippets
? _value.snippets
: snippets // ignore: cast_nullable_to_non_nullable
as Map<String, Object?>,
keys: null == keys
? _value.keys
: keys // ignore: cast_nullable_to_non_nullable
as Map<String, Object?>,
container: null == container
? _value.container
: container // ignore: cast_nullable_to_non_nullable
as Map<String, Object?>,
history: null == history
? _value.history
: history // ignore: cast_nullable_to_non_nullable
as Map<String, Object?>,
settings: null == settings
? _value.settings
: settings // ignore: cast_nullable_to_non_nullable
as Map<String, Object?>,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$BackupV2ImplCopyWith<$Res>
implements $BackupV2CopyWith<$Res> {
factory _$$BackupV2ImplCopyWith(
_$BackupV2Impl value,
$Res Function(_$BackupV2Impl) then,
) = __$$BackupV2ImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
int version,
int date,
Map<String, Object?> spis,
Map<String, Object?> snippets,
Map<String, Object?> keys,
Map<String, Object?> container,
Map<String, Object?> history,
Map<String, Object?> settings,
});
}
/// @nodoc
class __$$BackupV2ImplCopyWithImpl<$Res>
extends _$BackupV2CopyWithImpl<$Res, _$BackupV2Impl>
implements _$$BackupV2ImplCopyWith<$Res> {
__$$BackupV2ImplCopyWithImpl(
_$BackupV2Impl _value,
$Res Function(_$BackupV2Impl) _then,
) : super(_value, _then);
/// Create a copy of BackupV2
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? version = null,
Object? date = null,
Object? spis = null,
Object? snippets = null,
Object? keys = null,
Object? container = null,
Object? history = null,
Object? settings = null,
}) {
return _then(
_$BackupV2Impl(
version: null == version
? _value.version
: version // ignore: cast_nullable_to_non_nullable
as int,
date: null == date
? _value.date
: date // ignore: cast_nullable_to_non_nullable
as int,
spis: null == spis
? _value._spis
: spis // ignore: cast_nullable_to_non_nullable
as Map<String, Object?>,
snippets: null == snippets
? _value._snippets
: snippets // ignore: cast_nullable_to_non_nullable
as Map<String, Object?>,
keys: null == keys
? _value._keys
: keys // ignore: cast_nullable_to_non_nullable
as Map<String, Object?>,
container: null == container
? _value._container
: container // ignore: cast_nullable_to_non_nullable
as Map<String, Object?>,
history: null == history
? _value._history
: history // ignore: cast_nullable_to_non_nullable
as Map<String, Object?>,
settings: null == settings
? _value._settings
: settings // ignore: cast_nullable_to_non_nullable
as Map<String, Object?>,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$BackupV2Impl extends _BackupV2 {
const _$BackupV2Impl({
required this.version,
required this.date,
required final Map<String, Object?> spis,
required final Map<String, Object?> snippets,
required final Map<String, Object?> keys,
required final Map<String, Object?> container,
required final Map<String, Object?> history,
required final Map<String, Object?> settings,
}) : _spis = spis,
_snippets = snippets,
_keys = keys,
_container = container,
_history = history,
_settings = settings,
super._();
factory _$BackupV2Impl.fromJson(Map<String, dynamic> json) =>
_$$BackupV2ImplFromJson(json);
@override
final int version;
@override
final int date;
final Map<String, Object?> _spis;
@override
Map<String, Object?> get spis {
if (_spis is EqualUnmodifiableMapView) return _spis;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_spis);
}
final Map<String, Object?> _snippets;
@override
Map<String, Object?> get snippets {
if (_snippets is EqualUnmodifiableMapView) return _snippets;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_snippets);
}
final Map<String, Object?> _keys;
@override
Map<String, Object?> get keys {
if (_keys is EqualUnmodifiableMapView) return _keys;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_keys);
}
final Map<String, Object?> _container;
@override
Map<String, Object?> get container {
if (_container is EqualUnmodifiableMapView) return _container;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_container);
}
final Map<String, Object?> _history;
@override
Map<String, Object?> get history {
if (_history is EqualUnmodifiableMapView) return _history;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_history);
}
final Map<String, Object?> _settings;
@override
Map<String, Object?> get settings {
if (_settings is EqualUnmodifiableMapView) return _settings;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_settings);
}
@override
String toString() {
return 'BackupV2(version: $version, date: $date, spis: $spis, snippets: $snippets, keys: $keys, container: $container, history: $history, settings: $settings)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$BackupV2Impl &&
(identical(other.version, version) || other.version == version) &&
(identical(other.date, date) || other.date == date) &&
const DeepCollectionEquality().equals(other._spis, _spis) &&
const DeepCollectionEquality().equals(other._snippets, _snippets) &&
const DeepCollectionEquality().equals(other._keys, _keys) &&
const DeepCollectionEquality().equals(
other._container,
_container,
) &&
const DeepCollectionEquality().equals(other._history, _history) &&
const DeepCollectionEquality().equals(other._settings, _settings));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
version,
date,
const DeepCollectionEquality().hash(_spis),
const DeepCollectionEquality().hash(_snippets),
const DeepCollectionEquality().hash(_keys),
const DeepCollectionEquality().hash(_container),
const DeepCollectionEquality().hash(_history),
const DeepCollectionEquality().hash(_settings),
);
/// Create a copy of BackupV2
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$BackupV2ImplCopyWith<_$BackupV2Impl> get copyWith =>
__$$BackupV2ImplCopyWithImpl<_$BackupV2Impl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$BackupV2ImplToJson(this);
}
}
abstract class _BackupV2 extends BackupV2 {
const factory _BackupV2({
required final int version,
required final int date,
required final Map<String, Object?> spis,
required final Map<String, Object?> snippets,
required final Map<String, Object?> keys,
required final Map<String, Object?> container,
required final Map<String, Object?> history,
required final Map<String, Object?> settings,
}) = _$BackupV2Impl;
const _BackupV2._() : super._();
factory _BackupV2.fromJson(Map<String, dynamic> json) =
_$BackupV2Impl.fromJson;
@override
int get version;
@override
int get date;
@override
Map<String, Object?> get spis;
@override
Map<String, Object?> get snippets;
@override
Map<String, Object?> get keys;
@override
Map<String, Object?> get container;
@override
Map<String, Object?> get history;
@override
Map<String, Object?> get settings;
/// Create a copy of BackupV2
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$BackupV2ImplCopyWith<_$BackupV2Impl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,31 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'backup2.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$BackupV2Impl _$$BackupV2ImplFromJson(Map<String, dynamic> json) =>
_$BackupV2Impl(
version: (json['version'] as num).toInt(),
date: (json['date'] as num).toInt(),
spis: json['spis'] as Map<String, dynamic>,
snippets: json['snippets'] as Map<String, dynamic>,
keys: json['keys'] as Map<String, dynamic>,
container: json['container'] as Map<String, dynamic>,
history: json['history'] as Map<String, dynamic>,
settings: json['settings'] as Map<String, dynamic>,
);
Map<String, dynamic> _$$BackupV2ImplToJson(_$BackupV2Impl instance) =>
<String, dynamic>{
'version': instance.version,
'date': instance.date,
'spis': instance.spis,
'snippets': instance.snippets,
'keys': instance.keys,
'container': instance.container,
'history': instance.history,
'settings': instance.settings,
};

View File

@@ -0,0 +1,15 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/app/bak/backup.dart';
import 'package:server_box/data/model/app/bak/backup2.dart';
abstract final class MergeableUtils {
static (Mergeable, String) fromJsonString(String json) {
try {
final bak = BackupV2.fromJsonString(json);
return (bak, DateTime.fromMillisecondsSinceEpoch(bak.date).hms());
} catch (e) {
final bak = Backup.fromJsonString(json);
return (bak, bak.date);
}
}
}