#157 new: icloud sync

This commit is contained in:
lollipopkit
2023-09-11 19:20:29 +08:00
parent ddaee7c2f3
commit e4fd75ac5a
22 changed files with 448 additions and 125 deletions

View File

@@ -119,4 +119,4 @@ Future<void> _rmDownloadApks() async {
}
}
Future<String> get _dlDir async => joinPath((await docDir).path, 'Download');
Future<String> get _dlDir async => joinPath(await docDir, 'Download');

View File

@@ -0,0 +1,78 @@
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();
}

159
lib/core/utils/icloud.dart Normal file
View File

@@ -0,0 +1,159 @@
import 'dart:async';
import 'dart:io';
import 'package:icloud_storage/icloud_storage.dart';
import 'package:logging/logging.dart';
import '../../data/model/app/error.dart';
import '../../data/model/app/json.dart';
import '../../data/res/path.dart';
final _logger = Logger('iCloud');
class ICloud {
static const _containerId = 'iCloud.tech.lolli.serverbox';
const ICloud();
/// Upload file to iCloud
///
/// - [relativePath] is the path relative to [docDir],
/// must not starts with `/`
/// - [localPath] has higher priority than [relativePath], but only apply
/// to the local path instead of iCloud path
///
/// Return `null` if upload success, `ICloudErr` otherwise
static Future<ICloudErr?> upload({
required String relativePath,
String? localPath,
}) async {
final completer = Completer<ICloudErr?>();
await ICloudStorage.upload(
containerId: _containerId,
filePath: localPath ?? '${await docDir}/$relativePath',
destinationRelativePath: relativePath,
onProgress: (stream) {
stream.listen(
null,
onDone: () => completer.complete(null),
onError: (e) => completer.complete(
ICloudErr(type: ICloudErrType.generic, message: '$e'),
),
);
},
);
return completer.future;
}
static Future<List<ICloudFile>> getAll() async {
return await ICloudStorage.gather(
containerId: _containerId,
);
}
static Future<void> delete(String relativePath) async {
await ICloudStorage.delete(
containerId: _containerId,
relativePath: relativePath,
);
}
/// Download file from iCloud
///
/// - [relativePath] is the path relative to [docDir],
/// must not starts with `/`
/// - [localPath] has higher priority than [relativePath], but only apply
/// to the local path instead of iCloud path
///
/// Return `null` if upload success, `ICloudErr` otherwise
static Future<ICloudErr?> download({
required String relativePath,
String? localPath,
}) async {
final completer = Completer<ICloudErr?>();
await ICloudStorage.download(
containerId: _containerId,
relativePath: relativePath,
destinationFilePath: localPath ?? '${await docDir}/$relativePath',
onProgress: (stream) {
stream.listen(
null,
onDone: () => completer.complete(null),
onError: (e) => completer.complete(
ICloudErr(type: ICloudErrType.generic, message: '$e'),
),
);
},
);
return completer.future;
}
/// Sync file between iCloud and local
///
/// - [relativePath] is the path relative to [docDir],
/// must not starts with `/`
///
/// Return `null` if upload success, `ICloudErr` otherwise
///
/// TODO: consider merge strategy, use [SyncAble] and [JsonSerializable]
static Future<Iterable<ICloudErr>?> sync({
required Iterable<String> relativePaths,
}) async {
try {
final errs = <ICloudErr>[];
final allFiles = await getAll();
// remove files not in relativePaths
allFiles.removeWhere((e) => !relativePaths.contains(e.relativePath));
// upload files not in iCloud
final missed = relativePaths.where((e) {
return !allFiles.any((f) => f.relativePath == e);
});
for (final e in missed) {
final err = await upload(relativePath: e);
if (err != null) {
errs.add(err);
}
}
final docPath = await docDir;
// compare files in iCloud and local
for (final file in allFiles) {
final relativePath = file.relativePath;
/// Check date
final localFile = File('$docPath/$relativePath');
if (!localFile.existsSync()) {
/// Local file not found, download remote file
final err = await download(relativePath: relativePath);
if (err != null) {
errs.add(err);
}
continue;
}
final localDate = await localFile.lastModified();
if (file.contentChangeDate.isBefore(localDate)) {
/// Local is newer than remote, so upload local file
final err = await upload(relativePath: relativePath);
if (err != null) {
errs.add(err);
}
continue;
}
/// Remote is newer than local, so download remote
final err = await download(relativePath: relativePath);
if (err != null) {
errs.add(err);
}
}
_logger.info('Errs: $errs');
return errs.isEmpty ? null : errs;
} catch (e, s) {
_logger.warning('Sync failed: $relativePaths', e, s);
return [ICloudErr(type: ICloudErrType.generic, message: '$e')];
}
}
}

View File

@@ -43,6 +43,15 @@ void showSnackBarWithAction(
));
}
void showRestartSnackbar(BuildContext context, S s) {
showSnackBarWithAction(
context,
'${s.success}\n${s.needRestart}',
s.restart,
() => rebuildAll(context),
);
}
Future<bool> openUrl(String url) async {
return await launchUrl(url.uri, mode: LaunchMode.externalApplication);
}