opt.: icloud sync (#187)

This commit is contained in:
lollipopkit
2023-12-04 10:44:51 +08:00
parent 5035fdce86
commit 3524d92013
9 changed files with 73 additions and 11 deletions

View File

@@ -1,12 +1,13 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:icloud_storage/icloud_storage.dart';
import 'package:toolbox/data/model/app/backup.dart';
import 'package:toolbox/data/model/app/sync.dart';
import 'package:toolbox/data/res/logger.dart';
import '../../data/model/app/error.dart';
import '../../data/model/app/json.dart';
import '../../data/res/path.dart';
abstract final class ICloud {
@@ -93,8 +94,6 @@ abstract final class ICloud {
/// All files downloaded from cloud will be suffixed with [bakSuffix].
///
/// Return `null` if upload success, `ICloudErr` otherwise
///
/// TODO: consider merge strategy, use [SyncAble] and [JsonSerializable]
static Future<SyncResult<String, ICloudErr>> syncFiles({
required Iterable<String> relativePaths,
String? bakPrefix,
@@ -181,4 +180,35 @@ abstract final class ICloud {
}
}
static Future<void> sync() async {
try {
final result = await download(relativePath: Paths.bakName);
if (result != null) {
Loggers.app.warning('Download backup failed: $result');
return;
}
} catch (e, s) {
Loggers.app.warning('Download backup failed', e, s);
}
final dlFile = await File(await Paths.bak).readAsString();
final dlBak = await compute(Backup.fromJsonString, dlFile);
final restore = await dlBak.restore();
switch (restore) {
case true:
Loggers.app.info('Restore from iCloud (${dlBak.lastModTime}) success');
break;
case false:
await Backup.backup();
final uploadResult = await upload(relativePath: Paths.bakName);
if (uploadResult != null) {
Loggers.app.warning('Upload iCloud backup failed: $uploadResult');
} else {
Loggers.app.info('Upload iCloud backup success');
}
break;
case null:
Loggers.app.info('Skip iCloud sync');
break;
}
}
}

View File

@@ -20,6 +20,7 @@ class Backup {
final List<PrivateKeyInfo> keys;
final Map<String, dynamic> dockerHosts;
final Map<String, dynamic> settings;
final int? lastModTime;
const Backup({
required this.version,
@@ -29,6 +30,7 @@ class Backup {
required this.keys,
required this.dockerHosts,
required this.settings,
this.lastModTime,
});
Backup.fromJson(Map<String, dynamic> json)
@@ -43,7 +45,8 @@ class Backup {
.map((e) => PrivateKeyInfo.fromJson(e))
.toList(),
dockerHosts = json['dockerHosts'] ?? {},
settings = json['settings'] ?? {};
settings = json['settings'] ?? {},
lastModTime = json['lastModTime'];
Map<String, dynamic> toJson() => {
'version': version,
@@ -53,6 +56,7 @@ class Backup {
'keys': keys,
'dockerHosts': dockerHosts,
'settings': settings,
'lastModTime': lastModTime,
};
Backup.loadFromStore()
@@ -62,7 +66,8 @@ class Backup {
snippets = Stores.snippet.fetch(),
keys = Stores.key.fetch(),
dockerHosts = Stores.docker.box.toJson(),
settings = Stores.setting.box.toJson();
settings = Stores.setting.box.toJson(),
lastModTime = Stores.lastModTime;
static Future<String> backup() async {
final result = _diyEncrypt(json.encode(Backup.loadFromStore()));
@@ -71,7 +76,15 @@ class Backup {
return path;
}
Future<void> restore() async {
Future<bool?> restore({bool force = false}) async {
final curTime = Stores.lastModTime ?? 0;
final thisTime = lastModTime ?? 0;
if (curTime == thisTime) {
return null;
}
if (curTime > thisTime && !force) {
return false;
}
for (final s in settings.keys) {
Stores.setting.box.put(s, settings[s]);
}
@@ -90,6 +103,7 @@ class Backup {
Stores.docker.put(k, val);
}
}
return true;
}
Backup.fromJsonString(String raw)

View File

@@ -46,7 +46,9 @@ abstract final class Paths {
return _fontDir!;
}
static Future<String> get bak async => '${await doc}/srvbox_bak.json';
static const String bakName = 'srvbox_bak.json';
static Future<String> get bak async => '${await doc}/$bakName';
static Future<String> get dl async => joinPath(await doc, 'dl');
}

View File

@@ -23,4 +23,15 @@ abstract final class Stores {
key,
snippet,
];
static int? get lastModTime {
int? lastModTime = 0;
for (final store in all) {
final last = store.box.lastModified ?? 0;
if (last > (lastModTime ?? 0)) {
lastModTime = last;
}
}
return lastModTime;
}
}

View File

@@ -13,7 +13,7 @@ class PrivateKeyStore extends PersistentStore {
final ps = <PrivateKeyInfo>[];
for (final key in keys) {
final s = box.get(key);
if (s != null) {
if (s != null && s is PrivateKeyInfo) {
ps.add(s);
}
}

View File

@@ -13,7 +13,7 @@ class ServerStore extends PersistentStore {
final List<ServerPrivateInfo> ss = [];
for (final id in ids) {
final s = box.get(id);
if (s != null) {
if (s != null && s is ServerPrivateInfo) {
ss.add(s);
}
}

View File

@@ -13,7 +13,7 @@ class SnippetStore extends PersistentStore {
final ss = <Snippet>[];
for (final key in keys) {
final s = box.get(key);
if (s != null) {
if (s != null && s is Snippet) {
ss.add(s);
}
}

View File

@@ -9,6 +9,7 @@ import 'package:macos_window_utils/window_manipulator.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:toolbox/core/channel/bg_run.dart';
import 'package:toolbox/core/utils/icloud.dart';
import 'package:toolbox/core/utils/platform/base.dart';
import 'package:toolbox/data/res/logger.dart';
import 'package:toolbox/data/res/provider.dart';
@@ -91,6 +92,9 @@ Future<void> initApp() async {
// SharedPreferences is only used on Android for saving home widgets settings.
SharedPreferences.setPrefix('');
}
if (isIOS || isMacOS) {
if (Stores.setting.icloudSync.fetch()) ICloud.sync();
}
}
void _setupProviders() {

View File

@@ -190,7 +190,8 @@ class BackupPage extends StatelessWidget {
),
TextButton(
onPressed: () async {
await backup.restore();
/// TODO: add checkbox for not force restore
await backup.restore(force: true);
Pros.reload();
context.pop();
RebuildNodes.app.rebuild();