mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 15:24:35 +01:00
opt.: icloud sync (#187)
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:icloud_storage/icloud_storage.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/model/app/sync.dart';
|
||||||
import 'package:toolbox/data/res/logger.dart';
|
import 'package:toolbox/data/res/logger.dart';
|
||||||
|
|
||||||
import '../../data/model/app/error.dart';
|
import '../../data/model/app/error.dart';
|
||||||
import '../../data/model/app/json.dart';
|
|
||||||
import '../../data/res/path.dart';
|
import '../../data/res/path.dart';
|
||||||
|
|
||||||
abstract final class ICloud {
|
abstract final class ICloud {
|
||||||
@@ -93,8 +94,6 @@ abstract final class ICloud {
|
|||||||
/// All files downloaded from cloud will be suffixed with [bakSuffix].
|
/// All files downloaded from cloud will be suffixed with [bakSuffix].
|
||||||
///
|
///
|
||||||
/// Return `null` if upload success, `ICloudErr` otherwise
|
/// Return `null` if upload success, `ICloudErr` otherwise
|
||||||
///
|
|
||||||
/// TODO: consider merge strategy, use [SyncAble] and [JsonSerializable]
|
|
||||||
static Future<SyncResult<String, ICloudErr>> syncFiles({
|
static Future<SyncResult<String, ICloudErr>> syncFiles({
|
||||||
required Iterable<String> relativePaths,
|
required Iterable<String> relativePaths,
|
||||||
String? bakPrefix,
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class Backup {
|
|||||||
final List<PrivateKeyInfo> keys;
|
final List<PrivateKeyInfo> keys;
|
||||||
final Map<String, dynamic> dockerHosts;
|
final Map<String, dynamic> dockerHosts;
|
||||||
final Map<String, dynamic> settings;
|
final Map<String, dynamic> settings;
|
||||||
|
final int? lastModTime;
|
||||||
|
|
||||||
const Backup({
|
const Backup({
|
||||||
required this.version,
|
required this.version,
|
||||||
@@ -29,6 +30,7 @@ class Backup {
|
|||||||
required this.keys,
|
required this.keys,
|
||||||
required this.dockerHosts,
|
required this.dockerHosts,
|
||||||
required this.settings,
|
required this.settings,
|
||||||
|
this.lastModTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
Backup.fromJson(Map<String, dynamic> json)
|
Backup.fromJson(Map<String, dynamic> json)
|
||||||
@@ -43,7 +45,8 @@ class Backup {
|
|||||||
.map((e) => PrivateKeyInfo.fromJson(e))
|
.map((e) => PrivateKeyInfo.fromJson(e))
|
||||||
.toList(),
|
.toList(),
|
||||||
dockerHosts = json['dockerHosts'] ?? {},
|
dockerHosts = json['dockerHosts'] ?? {},
|
||||||
settings = json['settings'] ?? {};
|
settings = json['settings'] ?? {},
|
||||||
|
lastModTime = json['lastModTime'];
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'version': version,
|
'version': version,
|
||||||
@@ -53,6 +56,7 @@ class Backup {
|
|||||||
'keys': keys,
|
'keys': keys,
|
||||||
'dockerHosts': dockerHosts,
|
'dockerHosts': dockerHosts,
|
||||||
'settings': settings,
|
'settings': settings,
|
||||||
|
'lastModTime': lastModTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
Backup.loadFromStore()
|
Backup.loadFromStore()
|
||||||
@@ -62,7 +66,8 @@ class Backup {
|
|||||||
snippets = Stores.snippet.fetch(),
|
snippets = Stores.snippet.fetch(),
|
||||||
keys = Stores.key.fetch(),
|
keys = Stores.key.fetch(),
|
||||||
dockerHosts = Stores.docker.box.toJson(),
|
dockerHosts = Stores.docker.box.toJson(),
|
||||||
settings = Stores.setting.box.toJson();
|
settings = Stores.setting.box.toJson(),
|
||||||
|
lastModTime = Stores.lastModTime;
|
||||||
|
|
||||||
static Future<String> backup() async {
|
static Future<String> backup() async {
|
||||||
final result = _diyEncrypt(json.encode(Backup.loadFromStore()));
|
final result = _diyEncrypt(json.encode(Backup.loadFromStore()));
|
||||||
@@ -71,7 +76,15 @@ class Backup {
|
|||||||
return path;
|
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) {
|
for (final s in settings.keys) {
|
||||||
Stores.setting.box.put(s, settings[s]);
|
Stores.setting.box.put(s, settings[s]);
|
||||||
}
|
}
|
||||||
@@ -90,6 +103,7 @@ class Backup {
|
|||||||
Stores.docker.put(k, val);
|
Stores.docker.put(k, val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Backup.fromJsonString(String raw)
|
Backup.fromJsonString(String raw)
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ abstract final class Paths {
|
|||||||
return _fontDir!;
|
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');
|
static Future<String> get dl async => joinPath(await doc, 'dl');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,4 +23,15 @@ abstract final class Stores {
|
|||||||
key,
|
key,
|
||||||
snippet,
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class PrivateKeyStore extends PersistentStore {
|
|||||||
final ps = <PrivateKeyInfo>[];
|
final ps = <PrivateKeyInfo>[];
|
||||||
for (final key in keys) {
|
for (final key in keys) {
|
||||||
final s = box.get(key);
|
final s = box.get(key);
|
||||||
if (s != null) {
|
if (s != null && s is PrivateKeyInfo) {
|
||||||
ps.add(s);
|
ps.add(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class ServerStore extends PersistentStore {
|
|||||||
final List<ServerPrivateInfo> ss = [];
|
final List<ServerPrivateInfo> ss = [];
|
||||||
for (final id in ids) {
|
for (final id in ids) {
|
||||||
final s = box.get(id);
|
final s = box.get(id);
|
||||||
if (s != null) {
|
if (s != null && s is ServerPrivateInfo) {
|
||||||
ss.add(s);
|
ss.add(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class SnippetStore extends PersistentStore {
|
|||||||
final ss = <Snippet>[];
|
final ss = <Snippet>[];
|
||||||
for (final key in keys) {
|
for (final key in keys) {
|
||||||
final s = box.get(key);
|
final s = box.get(key);
|
||||||
if (s != null) {
|
if (s != null && s is Snippet) {
|
||||||
ss.add(s);
|
ss.add(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:macos_window_utils/window_manipulator.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:toolbox/core/channel/bg_run.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/core/utils/platform/base.dart';
|
||||||
import 'package:toolbox/data/res/logger.dart';
|
import 'package:toolbox/data/res/logger.dart';
|
||||||
import 'package:toolbox/data/res/provider.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 is only used on Android for saving home widgets settings.
|
||||||
SharedPreferences.setPrefix('');
|
SharedPreferences.setPrefix('');
|
||||||
}
|
}
|
||||||
|
if (isIOS || isMacOS) {
|
||||||
|
if (Stores.setting.icloudSync.fetch()) ICloud.sync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setupProviders() {
|
void _setupProviders() {
|
||||||
|
|||||||
@@ -190,7 +190,8 @@ class BackupPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await backup.restore();
|
/// TODO: add checkbox for not force restore
|
||||||
|
await backup.restore(force: true);
|
||||||
Pros.reload();
|
Pros.reload();
|
||||||
context.pop();
|
context.pop();
|
||||||
RebuildNodes.app.rebuild();
|
RebuildNodes.app.rebuild();
|
||||||
|
|||||||
Reference in New Issue
Block a user