From 3524d920131a1f337ea1c52e769bf5a8b4e67971 Mon Sep 17 00:00:00 2001 From: lollipopkit Date: Mon, 4 Dec 2023 10:44:51 +0800 Subject: [PATCH] opt.: `icloud sync` (#187) --- lib/core/utils/icloud.dart | 36 ++++++++++++++++++++++++++++++--- lib/data/model/app/backup.dart | 20 +++++++++++++++--- lib/data/res/path.dart | 4 +++- lib/data/res/store.dart | 11 ++++++++++ lib/data/store/private_key.dart | 2 +- lib/data/store/server.dart | 2 +- lib/data/store/snippet.dart | 2 +- lib/main.dart | 4 ++++ lib/view/page/backup.dart | 3 ++- 9 files changed, 73 insertions(+), 11 deletions(-) diff --git a/lib/core/utils/icloud.dart b/lib/core/utils/icloud.dart index 10fa394f..e375eb65 100644 --- a/lib/core/utils/icloud.dart +++ b/lib/core/utils/icloud.dart @@ -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> syncFiles({ required Iterable relativePaths, String? bakPrefix, @@ -181,4 +180,35 @@ abstract final class ICloud { } } + static Future 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; + } + } } diff --git a/lib/data/model/app/backup.dart b/lib/data/model/app/backup.dart index f9706a17..99b47bbd 100644 --- a/lib/data/model/app/backup.dart +++ b/lib/data/model/app/backup.dart @@ -20,6 +20,7 @@ class Backup { final List keys; final Map dockerHosts; final Map 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 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 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 backup() async { final result = _diyEncrypt(json.encode(Backup.loadFromStore())); @@ -71,7 +76,15 @@ class Backup { return path; } - Future restore() async { + Future 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) diff --git a/lib/data/res/path.dart b/lib/data/res/path.dart index 23a6e8e7..79d8d9d9 100644 --- a/lib/data/res/path.dart +++ b/lib/data/res/path.dart @@ -46,7 +46,9 @@ abstract final class Paths { return _fontDir!; } - static Future get bak async => '${await doc}/srvbox_bak.json'; + static const String bakName = 'srvbox_bak.json'; + + static Future get bak async => '${await doc}/$bakName'; static Future get dl async => joinPath(await doc, 'dl'); } diff --git a/lib/data/res/store.dart b/lib/data/res/store.dart index 17046894..a6791723 100644 --- a/lib/data/res/store.dart +++ b/lib/data/res/store.dart @@ -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; + } } diff --git a/lib/data/store/private_key.dart b/lib/data/store/private_key.dart index 444e1c7a..2319266b 100644 --- a/lib/data/store/private_key.dart +++ b/lib/data/store/private_key.dart @@ -13,7 +13,7 @@ class PrivateKeyStore extends PersistentStore { final ps = []; for (final key in keys) { final s = box.get(key); - if (s != null) { + if (s != null && s is PrivateKeyInfo) { ps.add(s); } } diff --git a/lib/data/store/server.dart b/lib/data/store/server.dart index fdff6eb9..dc3f857a 100644 --- a/lib/data/store/server.dart +++ b/lib/data/store/server.dart @@ -13,7 +13,7 @@ class ServerStore extends PersistentStore { final List ss = []; for (final id in ids) { final s = box.get(id); - if (s != null) { + if (s != null && s is ServerPrivateInfo) { ss.add(s); } } diff --git a/lib/data/store/snippet.dart b/lib/data/store/snippet.dart index bf120d2b..2062dda2 100644 --- a/lib/data/store/snippet.dart +++ b/lib/data/store/snippet.dart @@ -13,7 +13,7 @@ class SnippetStore extends PersistentStore { final ss = []; for (final key in keys) { final s = box.get(key); - if (s != null) { + if (s != null && s is Snippet) { ss.add(s); } } diff --git a/lib/main.dart b/lib/main.dart index 27e457fe..a4068a26 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 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() { diff --git a/lib/view/page/backup.dart b/lib/view/page/backup.dart index 51eadff7..ba84a039 100644 --- a/lib/view/page/backup.dart +++ b/lib/view/page/backup.dart @@ -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();