opt.: backup

This commit is contained in:
lollipopkit
2023-09-13 13:05:19 +08:00
parent 9ce7138d9b
commit 269c2a0a10
37 changed files with 535 additions and 632 deletions

View File

@@ -1,78 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import '../../data/model/app/backup.dart';
import '../../data/res/path.dart';
import '../../data/store/docker.dart';
import '../../data/store/private_key.dart';
import '../../data/store/server.dart';
import '../../data/store/setting.dart';
import '../../data/store/snippet.dart';
import '../../locator.dart';
final _server = locator<ServerStore>();
final _snippet = locator<SnippetStore>();
final _privateKey = locator<PrivateKeyStore>();
final _dockerHosts = locator<DockerStore>();
final _setting = locator<SettingStore>();
Future<String> get backupPath async => '${await docDir}/srvbox_bak.json';
const backupFormatVersion = 1;
Future<void> backup() async {
final result = _diyEncrtpt(
json.encode(
Backup(
version: backupFormatVersion,
date: DateTime.now().toString().split('.').first,
spis: _server.fetch(),
snippets: _snippet.fetch(),
keys: _privateKey.fetch(),
dockerHosts: _dockerHosts.fetchAll(),
settings: _setting.toJson(),
),
),
);
await File(await backupPath).writeAsString(result);
}
void restore(Backup backup) {
for (final s in backup.snippets) {
_snippet.put(s);
}
for (final s in backup.spis) {
_server.put(s);
}
for (final s in backup.keys) {
_privateKey.put(s);
}
for (final k in backup.dockerHosts.keys) {
final val = backup.dockerHosts[k];
if (val != null && val is String && val.isNotEmpty) {
_dockerHosts.put(k, val);
}
}
}
Future<Backup> decodeBackup(String raw) async {
return await compute(_decode, raw);
}
Backup _decode(String raw) {
final decrypted = _diyDecrypt(raw);
return Backup.fromJson(json.decode(decrypted));
}
String _diyEncrtpt(String raw) =>
json.encode(raw.codeUnits.map((e) => e * 2 + 1).toList(growable: false));
String _diyDecrypt(String raw) {
final list = json.decode(raw);
final sb = StringBuffer();
for (final e in list) {
sb.writeCharCode((e - 1) ~/ 2);
}
return sb.toString();
}

View File

@@ -100,6 +100,9 @@ class ICloud {
static Future<Iterable<ICloudErr>?> sync({
required Iterable<String> relativePaths,
}) async {
final uploadFiles = <String>[];
final downloadFiles = <String>[];
try {
final errs = <ICloudErr>[];
@@ -147,11 +150,13 @@ class ICloud {
/// Local is newer than remote, so upload local file
if (remoteDate.isBefore(localDate)) {
await delete(relativePath);
final err = await upload(relativePath: relativePath);
if (err != null) {
errs.add(err);
}
//_logger.info('local newer: $relativePath');
uploadFiles.add(relativePath);
return;
}
@@ -161,6 +166,7 @@ class ICloud {
errs.add(err);
}
//_logger.info('remote newer: $relativePath');
downloadFiles.add(relativePath);
}));
await Future.wait(mission);
@@ -170,18 +176,18 @@ class ICloud {
_logger.warning('Sync failed: $relativePaths', e, s);
return [ICloudErr(type: ICloudErrType.generic, message: '$e')];
} finally {
_logger.info('Sync finished.');
_logger.info('Sync upload: $uploadFiles, download: $downloadFiles');
}
}
}
Future<void> syncApple() async {
if (!isIOS && !isMacOS) return;
final docPath = await docDir;
final dir = Directory(docPath);
final files = await dir.list().toList();
// filter out non-hive(db) files
files.removeWhere((e) => !e.path.endsWith('.hive'));
final paths = files.map((e) => e.path.replaceFirst('$docPath/', ''));
await ICloud.sync(relativePaths: paths);
static Future<void> syncDb() async {
if (!isIOS && !isMacOS) return;
final docPath = await docDir;
final dir = Directory(docPath);
final files = await dir.list().toList();
// filter out non-hive(db) files
files.removeWhere((e) => !e.path.endsWith('.hive'));
final paths = files.map((e) => e.path.replaceFirst('$docPath/', ''));
await ICloud.sync(relativePaths: paths);
}
}

View File

@@ -9,7 +9,6 @@ import 'package:share_plus/share_plus.dart';
import '../../data/provider/app.dart';
import '../../locator.dart';
import '../../view/widget/rebuild.dart';
import 'platform.dart';
final _app = locator<AppProvider>();
@@ -64,10 +63,7 @@ String? getFileName(String? path) {
return path.split('/').last;
}
void rebuildAll(BuildContext context) {
RebuildWidget.restartApp(context);
}
/// Return fmt: 2021-01-01 00:00:00
String getTime(int? unixMill) {
return DateTime.fromMillisecondsSinceEpoch((unixMill ?? 0) * 1000)
.toString()

View File

@@ -3,125 +3,17 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:toolbox/core/extension/context.dart';
import 'package:toolbox/data/model/app/tab.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../data/model/server/snippet.dart';
import '../../data/provider/snippet.dart';
import '../../data/res/ui.dart';
import '../../locator.dart';
import '../../view/widget/input_field.dart';
import '../../view/widget/picker.dart';
import '../persistant_store.dart';
import '../route.dart';
import 'misc.dart';
import 'platform.dart';
import '../extension/stringx.dart';
import '../extension/uint8list.dart';
void showSnackBar(BuildContext context, Widget child) =>
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: child,
behavior: SnackBarBehavior.floating,
));
void showSnackBarWithAction(
BuildContext context,
String content,
String action,
GestureTapCallback onTap,
) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(content),
behavior: SnackBarBehavior.floating,
action: SnackBarAction(
label: action,
onPressed: onTap,
),
));
}
void showRestartSnackbar(BuildContext context, {String? btn, String? msg}) {
showSnackBarWithAction(
context,
msg ?? 'Need restart to take effect',
btn ?? 'Restart',
() => rebuildAll(context),
);
}
Future<bool> openUrl(String url) async {
return await launchUrl(url.uri, mode: LaunchMode.externalApplication);
}
Future<T?> showRoundDialog<T>({
required BuildContext context,
Widget? child,
List<Widget>? actions,
Widget? title,
bool barrierDismiss = true,
}) async {
return await showDialog<T>(
context: context,
barrierDismissible: barrierDismiss,
builder: (_) {
return AlertDialog(
title: title,
content: child,
actions: actions,
actionsPadding: const EdgeInsets.all(17),
);
},
);
}
void showLoadingDialog(BuildContext context, {bool barrierDismiss = false}) {
showRoundDialog(
context: context,
child: centerSizedLoading,
barrierDismiss: barrierDismiss,
);
}
Future<String?> showPwdDialog(
BuildContext context,
String? user,
) async {
if (!context.mounted) return null;
final s = S.of(context)!;
return await showRoundDialog<String>(
context: context,
title: Text(user ?? s.pwd),
child: Input(
autoFocus: true,
type: TextInputType.visiblePassword,
obscureText: true,
onSubmitted: (val) => context.pop(val.trim()),
label: s.pwd,
),
);
}
Widget buildSwitch(
BuildContext context,
StorePropertyBase<bool> prop, {
void Function(bool)? func,
}) {
return ValueListenableBuilder(
valueListenable: prop.listenable(),
builder: (context, bool value, widget) {
return Switch(
value: value,
onChanged: (value) {
if (func != null) func(value);
prop.put(value);
});
},
);
}
void setTransparentNavigationBar(BuildContext context) {
if (isAndroid) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
@@ -133,18 +25,6 @@ void setTransparentNavigationBar(BuildContext context) {
}
}
String tabTitleName(BuildContext context, AppTab tab) {
final s = S.of(context)!;
switch (tab) {
case AppTab.server:
return s.server;
case AppTab.snippet:
return s.convert;
case AppTab.ping:
return 'Ping';
}
}
Future<void> loadFontFile(String localPath) async {
if (localPath.isEmpty) return;
final name = getFileName(localPath);
@@ -156,53 +36,6 @@ Future<void> loadFontFile(String localPath) async {
await fontLoader.load();
}
void showSnippetDialog(
BuildContext context,
S s,
void Function(Snippet s) onSelected,
) {
final provider = locator<SnippetProvider>();
if (provider.snippets.isEmpty) {
showRoundDialog(
context: context,
child: Text(s.noSavedSnippet),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(s.ok),
),
TextButton(
onPressed: () {
context.pop();
AppRoute.snippetEdit().go(context);
},
child: Text(s.add),
)
],
);
return;
}
var snippet = provider.snippets.first;
showRoundDialog(
context: context,
title: Text(s.choose),
child: Picker(
items: provider.snippets.map((e) => Text(e.name)).toList(),
onSelected: (idx) => snippet = provider.snippets[idx],
),
actions: [
TextButton(
onPressed: () async {
context.pop();
onSelected(snippet);
},
child: Text(s.ok),
)
],
);
}
void switchStatusBar({required bool hide}) {
if (hide) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky,