mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
opt.: split webdav & other settings (#569)
This commit is contained in:
40
lib/core/sync.dart
Normal file
40
lib/core/sync.dart
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:server_box/data/model/app/backup.dart';
|
||||||
|
import 'package:server_box/data/store/no_backup.dart';
|
||||||
|
|
||||||
|
const sync = Sync._();
|
||||||
|
|
||||||
|
final class Sync extends SyncCfg {
|
||||||
|
const Sync._() : super();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveToFile() => Backup.backup();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Mergeable> fromFile(String path) async {
|
||||||
|
final content = await File(path).readAsString();
|
||||||
|
return Backup.fromJsonString(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<RemoteStorage?> get remoteStorage async {
|
||||||
|
if (isMacOS || isIOS) await icloud.init('iCloud.tech.lolli.serverbox');
|
||||||
|
final settings = NoBackupStore.instance;
|
||||||
|
await webdav.init(WebdavInitArgs(
|
||||||
|
url: settings.webdavUrl.fetch(),
|
||||||
|
user: settings.webdavUser.fetch(),
|
||||||
|
pwd: settings.webdavPwd.fetch(),
|
||||||
|
prefix: 'serverbox/',
|
||||||
|
));
|
||||||
|
|
||||||
|
final icloudEnabled = settings.icloudSync.fetch();
|
||||||
|
if (icloudEnabled) return icloud;
|
||||||
|
|
||||||
|
final webdavEnabled = settings.webdavSync.fetch();
|
||||||
|
if (webdavEnabled) return webdav;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:computer/computer.dart';
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
|
||||||
import 'package:icloud_storage/icloud_storage.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:server_box/data/model/app/backup.dart';
|
|
||||||
import 'package:server_box/data/model/app/sync.dart';
|
|
||||||
import 'package:server_box/data/res/misc.dart';
|
|
||||||
|
|
||||||
import 'package:server_box/data/model/app/error.dart';
|
|
||||||
|
|
||||||
abstract final class ICloud {
|
|
||||||
static const _containerId = 'iCloud.tech.lolli.serverbox';
|
|
||||||
|
|
||||||
static final _logger = Logger('iCloud');
|
|
||||||
|
|
||||||
/// Upload file to iCloud
|
|
||||||
///
|
|
||||||
/// - [relativePath] is the path relative to [Paths.doc],
|
|
||||||
/// 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?>();
|
|
||||||
try {
|
|
||||||
await ICloudStorage.upload(
|
|
||||||
containerId: _containerId,
|
|
||||||
filePath: localPath ?? '${Paths.doc}/$relativePath',
|
|
||||||
destinationRelativePath: relativePath,
|
|
||||||
onProgress: (stream) {
|
|
||||||
stream.listen(
|
|
||||||
null,
|
|
||||||
onDone: () => completer.complete(null),
|
|
||||||
onError: (e) => completer.complete(
|
|
||||||
ICloudErr(type: ICloudErrType.generic, message: '$e'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Upload $relativePath failed', e, s);
|
|
||||||
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 {
|
|
||||||
try {
|
|
||||||
await ICloudStorage.delete(
|
|
||||||
containerId: _containerId,
|
|
||||||
relativePath: relativePath,
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Delete $relativePath failed', e, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Download file from iCloud
|
|
||||||
///
|
|
||||||
/// - [relativePath] is the path relative to [Paths.doc],
|
|
||||||
/// 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?>();
|
|
||||||
try {
|
|
||||||
await ICloudStorage.download(
|
|
||||||
containerId: _containerId,
|
|
||||||
relativePath: relativePath,
|
|
||||||
destinationFilePath: localPath ?? '${Paths.doc}/$relativePath',
|
|
||||||
onProgress: (stream) {
|
|
||||||
stream.listen(
|
|
||||||
null,
|
|
||||||
onDone: () => completer.complete(null),
|
|
||||||
onError: (e) => completer.complete(
|
|
||||||
ICloudErr(type: ICloudErrType.generic, message: '$e'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Download $relativePath failed', e, s);
|
|
||||||
completer.complete(ICloudErr(type: ICloudErrType.generic, message: '$e'));
|
|
||||||
}
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sync file between iCloud and local
|
|
||||||
///
|
|
||||||
/// - [relativePaths] is the path relative to [Paths.doc],
|
|
||||||
/// must not starts with `/`
|
|
||||||
/// - [bakPrefix] is the suffix of backup file, default to [null].
|
|
||||||
/// All files downloaded from cloud will be suffixed with [bakPrefix].
|
|
||||||
///
|
|
||||||
/// Return `null` if upload success, [ICloudErr] otherwise
|
|
||||||
static Future<SyncResult<String, ICloudErr>> syncFiles({
|
|
||||||
required Iterable<String> relativePaths,
|
|
||||||
String? bakPrefix,
|
|
||||||
}) async {
|
|
||||||
final uploadFiles = <String>[];
|
|
||||||
final downloadFiles = <String>[];
|
|
||||||
|
|
||||||
try {
|
|
||||||
final errs = <String, ICloudErr>{};
|
|
||||||
|
|
||||||
final allFiles = await getAll();
|
|
||||||
|
|
||||||
/// remove files not in relativePaths
|
|
||||||
allFiles.removeWhere((e) => !relativePaths.contains(e.relativePath));
|
|
||||||
|
|
||||||
final missions = <Future<void>>[];
|
|
||||||
|
|
||||||
/// upload files not in iCloud
|
|
||||||
final missed = relativePaths.where((e) {
|
|
||||||
return !allFiles.any((f) => f.relativePath == e);
|
|
||||||
});
|
|
||||||
missions.addAll(missed.map((e) async {
|
|
||||||
final err = await upload(relativePath: e);
|
|
||||||
if (err != null) {
|
|
||||||
errs[e] = err;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
final docPath = Paths.doc;
|
|
||||||
|
|
||||||
/// compare files in iCloud and local
|
|
||||||
missions.addAll(allFiles.map((file) async {
|
|
||||||
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[relativePath] = err;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final localDate = await localFile.lastModified();
|
|
||||||
final remoteDate = file.contentChangeDate;
|
|
||||||
|
|
||||||
/// Same date, skip
|
|
||||||
if (remoteDate.difference(localDate) == Duration.zero) return;
|
|
||||||
|
|
||||||
/// 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[relativePath] = err;
|
|
||||||
}
|
|
||||||
uploadFiles.add(relativePath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remote is newer than local, so download remote
|
|
||||||
final localPath = '$docPath/${bakPrefix ?? ''}$relativePath';
|
|
||||||
final err = await download(
|
|
||||||
relativePath: relativePath,
|
|
||||||
localPath: localPath,
|
|
||||||
);
|
|
||||||
if (err != null) {
|
|
||||||
errs[relativePath] = err;
|
|
||||||
}
|
|
||||||
downloadFiles.add(relativePath);
|
|
||||||
}));
|
|
||||||
|
|
||||||
await Future.wait(missions);
|
|
||||||
|
|
||||||
return SyncResult(up: uploadFiles, down: downloadFiles, err: errs);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Sync: $relativePaths failed', e, s);
|
|
||||||
return SyncResult(up: uploadFiles, down: downloadFiles, err: {
|
|
||||||
'Generic': ICloudErr(type: ICloudErrType.generic, message: '$e')
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
_logger.info('Sync, up: $uploadFiles, down: $downloadFiles');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> sync() async {
|
|
||||||
final result = await download(relativePath: Miscs.bakFileName);
|
|
||||||
if (result != null) {
|
|
||||||
await backup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final dlFile = await File(Paths.bak).readAsString();
|
|
||||||
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
|
|
||||||
await dlBak.restore();
|
|
||||||
|
|
||||||
await backup();
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> backup() async {
|
|
||||||
await Backup.backup();
|
|
||||||
final uploadResult = await upload(relativePath: Miscs.bakFileName);
|
|
||||||
if (uploadResult != null) {
|
|
||||||
_logger.warning('Upload backup failed: $uploadResult');
|
|
||||||
} else {
|
|
||||||
_logger.info('Upload backup success');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:computer/computer.dart';
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:server_box/data/model/app/backup.dart';
|
|
||||||
import 'package:server_box/data/model/app/error.dart';
|
|
||||||
import 'package:server_box/data/res/misc.dart';
|
|
||||||
import 'package:server_box/data/res/store.dart';
|
|
||||||
import 'package:webdav_client/webdav_client.dart';
|
|
||||||
|
|
||||||
abstract final class Webdav {
|
|
||||||
/// Some WebDAV provider only support non-root path
|
|
||||||
static const _prefix = 'srvbox/';
|
|
||||||
|
|
||||||
static var _client = WebdavClient(
|
|
||||||
url: Stores.setting.webdavUrl.fetch(),
|
|
||||||
user: Stores.setting.webdavUser.fetch(),
|
|
||||||
pwd: Stores.setting.webdavPwd.fetch(),
|
|
||||||
);
|
|
||||||
|
|
||||||
static final _logger = Logger('Webdav');
|
|
||||||
|
|
||||||
static Future<String?> test(String url, String user, String pwd) async {
|
|
||||||
final client = WebdavClient(url: url, user: user, pwd: pwd);
|
|
||||||
try {
|
|
||||||
await client.ping();
|
|
||||||
return null;
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Test failed', e, s);
|
|
||||||
return e.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<WebdavErr?> upload({
|
|
||||||
required String relativePath,
|
|
||||||
String? localPath,
|
|
||||||
}) async {
|
|
||||||
try {
|
|
||||||
await _client.writeFile(
|
|
||||||
localPath ?? '${Paths.doc}/$relativePath',
|
|
||||||
_prefix + relativePath,
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Upload $relativePath failed', e, s);
|
|
||||||
return WebdavErr(type: WebdavErrType.generic, message: '$e');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<WebdavErr?> delete(String relativePath) async {
|
|
||||||
try {
|
|
||||||
await _client.remove(_prefix + relativePath);
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Delete $relativePath failed', e, s);
|
|
||||||
return WebdavErr(type: WebdavErrType.generic, message: '$e');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<WebdavErr?> download({
|
|
||||||
required String relativePath,
|
|
||||||
String? localPath,
|
|
||||||
}) async {
|
|
||||||
try {
|
|
||||||
await _client.readFile(
|
|
||||||
_prefix + relativePath,
|
|
||||||
localPath ?? '${Paths.doc}/$relativePath',
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
_logger.warning('Download $relativePath failed');
|
|
||||||
return WebdavErr(type: WebdavErrType.generic, message: '$e');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<List<String>> list() async {
|
|
||||||
try {
|
|
||||||
final list = await _client.readDir(_prefix);
|
|
||||||
final names = <String>[];
|
|
||||||
for (final item in list) {
|
|
||||||
if ((item.isDir ?? true) || item.name == null) continue;
|
|
||||||
names.add(item.name!);
|
|
||||||
}
|
|
||||||
return names;
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('List failed', e, s);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void changeClient(String url, String user, String pwd) {
|
|
||||||
_client = WebdavClient(url: url, user: user, pwd: pwd);
|
|
||||||
Stores.setting.webdavUrl.put(url);
|
|
||||||
Stores.setting.webdavUser.put(user);
|
|
||||||
Stores.setting.webdavPwd.put(pwd);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> sync() async {
|
|
||||||
final result = await download(relativePath: Miscs.bakFileName);
|
|
||||||
if (result != null) {
|
|
||||||
await backup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final dlFile = await File(Paths.bak).readAsString();
|
|
||||||
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
|
|
||||||
await dlBak.restore();
|
|
||||||
} catch (e) {
|
|
||||||
_logger.warning('Restore failed: $e');
|
|
||||||
}
|
|
||||||
|
|
||||||
await backup();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a local backup and upload it to WebDAV
|
|
||||||
static Future<void> backup() async {
|
|
||||||
await Backup.backup();
|
|
||||||
final uploadResult = await upload(relativePath: Miscs.bakFileName);
|
|
||||||
if (uploadResult != null) {
|
|
||||||
_logger.warning('Upload failed: $uploadResult');
|
|
||||||
} else {
|
|
||||||
_logger.info('Upload success');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@ const backupFormatVersion = 1;
|
|||||||
final _logger = Logger('Backup');
|
final _logger = Logger('Backup');
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Backup {
|
class Backup extends Mergeable {
|
||||||
// backup format version
|
// backup format version
|
||||||
final int version;
|
final int version;
|
||||||
final String date;
|
final String date;
|
||||||
@@ -28,8 +28,9 @@ class Backup {
|
|||||||
final Map<String, dynamic> container;
|
final Map<String, dynamic> container;
|
||||||
final Map<String, dynamic> history;
|
final Map<String, dynamic> history;
|
||||||
final int? lastModTime;
|
final int? lastModTime;
|
||||||
|
final Map<String, dynamic> settings;
|
||||||
|
|
||||||
const Backup({
|
Backup({
|
||||||
required this.version,
|
required this.version,
|
||||||
required this.date,
|
required this.date,
|
||||||
required this.spis,
|
required this.spis,
|
||||||
@@ -37,6 +38,7 @@ class Backup {
|
|||||||
required this.keys,
|
required this.keys,
|
||||||
required this.container,
|
required this.container,
|
||||||
required this.history,
|
required this.history,
|
||||||
|
required this.settings,
|
||||||
this.lastModTime,
|
this.lastModTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,7 +54,8 @@ class Backup {
|
|||||||
keys = Stores.key.fetch(),
|
keys = Stores.key.fetch(),
|
||||||
container = Stores.container.box.toJson(),
|
container = Stores.container.box.toJson(),
|
||||||
lastModTime = Stores.lastModTime,
|
lastModTime = Stores.lastModTime,
|
||||||
history = Stores.history.box.toJson();
|
history = Stores.history.box.toJson(),
|
||||||
|
settings = Stores.setting.box.toJson();
|
||||||
|
|
||||||
static Future<String> backup([String? name]) async {
|
static Future<String> backup([String? name]) async {
|
||||||
final result = _diyEncrypt(json.encode(Backup.loadFromStore().toJson()));
|
final result = _diyEncrypt(json.encode(Backup.loadFromStore().toJson()));
|
||||||
@@ -61,7 +64,8 @@ class Backup {
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> restore({bool force = false}) async {
|
@override
|
||||||
|
Future<void> merge({bool force = false}) async {
|
||||||
final curTime = Stores.lastModTime ?? 0;
|
final curTime = Stores.lastModTime ?? 0;
|
||||||
final bakTime = lastModTime ?? 0;
|
final bakTime = lastModTime ?? 0;
|
||||||
final shouldRestore = force || curTime < bakTime;
|
final shouldRestore = force || curTime < bakTime;
|
||||||
@@ -176,6 +180,26 @@ class Backup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
if (force) {
|
||||||
|
Stores.setting.box.putAll(settings);
|
||||||
|
} else {
|
||||||
|
final nowSettings = Stores.setting.box.keys.toSet();
|
||||||
|
final bakSettings = settings.keys.toSet();
|
||||||
|
final newSettings = bakSettings.difference(nowSettings);
|
||||||
|
final delSettings = nowSettings.difference(bakSettings);
|
||||||
|
final updateSettings = nowSettings.intersection(bakSettings);
|
||||||
|
for (final s in newSettings) {
|
||||||
|
Stores.setting.box.put(s, settings[s]);
|
||||||
|
}
|
||||||
|
for (final s in delSettings) {
|
||||||
|
Stores.setting.box.delete(s);
|
||||||
|
}
|
||||||
|
for (final s in updateSettings) {
|
||||||
|
Stores.setting.box.put(s, settings[s]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Provider.reload();
|
Provider.reload();
|
||||||
RNodes.app.notify();
|
RNodes.app.notify();
|
||||||
|
|
||||||
|
|||||||
16
lib/data/store/no_backup.dart
Normal file
16
lib/data/store/no_backup.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
|
||||||
|
final class NoBackupStore extends PersistentStore {
|
||||||
|
NoBackupStore._() : super('no_backup');
|
||||||
|
|
||||||
|
static final instance = NoBackupStore._();
|
||||||
|
|
||||||
|
/// Only valid on iOS and macOS
|
||||||
|
late final icloudSync = property('icloudSync', false);
|
||||||
|
|
||||||
|
/// Webdav sync
|
||||||
|
late final webdavSync = property('webdavSync', false);
|
||||||
|
late final webdavUrl = property('webdavUrl', '');
|
||||||
|
late final webdavUser = property('webdavUser', '');
|
||||||
|
late final webdavPwd = property('webdavPwd', '');
|
||||||
|
}
|
||||||
@@ -125,8 +125,6 @@ class SettingStore extends PersistentStore {
|
|||||||
/// Whether use system's primary color as the app's primary color
|
/// Whether use system's primary color as the app's primary color
|
||||||
late final useSystemPrimaryColor = property('useSystemPrimaryColor', false);
|
late final useSystemPrimaryColor = property('useSystemPrimaryColor', false);
|
||||||
|
|
||||||
/// Only valid on iOS and macOS
|
|
||||||
late final icloudSync = property('icloudSync', false);
|
|
||||||
|
|
||||||
/// Only valid on iOS / Android / Windows
|
/// Only valid on iOS / Android / Windows
|
||||||
late final useBioAuth = property('useBioAuth', false);
|
late final useBioAuth = property('useBioAuth', false);
|
||||||
@@ -143,12 +141,6 @@ class SettingStore extends PersistentStore {
|
|||||||
/// Show tip of suspend
|
/// Show tip of suspend
|
||||||
late final showSuspendTip = property('showSuspendTip', true);
|
late final showSuspendTip = property('showSuspendTip', true);
|
||||||
|
|
||||||
/// Webdav sync
|
|
||||||
late final webdavSync = property('webdavSync', false);
|
|
||||||
late final webdavUrl = property('webdavUrl', '', updateLastModified: false);
|
|
||||||
late final webdavUser = property('webdavUser', '', updateLastModified: false);
|
|
||||||
late final webdavPwd = property('webdavPwd', '', updateLastModified: false);
|
|
||||||
|
|
||||||
/// Whether collapse UI items by default
|
/// Whether collapse UI items by default
|
||||||
late final collapseUIDefault = property('collapseUIDefault', true);
|
late final collapseUIDefault = property('collapseUIDefault', true);
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import 'package:hive_flutter/hive_flutter.dart';
|
|||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:server_box/app.dart';
|
import 'package:server_box/app.dart';
|
||||||
import 'package:server_box/core/channel/bg_run.dart';
|
import 'package:server_box/core/channel/bg_run.dart';
|
||||||
import 'package:server_box/core/utils/sync/icloud.dart';
|
import 'package:server_box/core/sync.dart';
|
||||||
import 'package:server_box/core/utils/sync/webdav.dart';
|
|
||||||
import 'package:server_box/data/model/app/menu/server_func.dart';
|
import 'package:server_box/data/model/app/menu/server_func.dart';
|
||||||
import 'package:server_box/data/model/app/net_view.dart';
|
import 'package:server_box/data/model/app/net_view.dart';
|
||||||
import 'package:server_box/data/model/app/server_detail_card.dart';
|
import 'package:server_box/data/model/app/server_detail_card.dart';
|
||||||
@@ -28,7 +27,6 @@ import 'package:server_box/data/provider/snippet.dart';
|
|||||||
import 'package:server_box/data/res/build_data.dart';
|
import 'package:server_box/data/res/build_data.dart';
|
||||||
import 'package:server_box/data/res/misc.dart';
|
import 'package:server_box/data/res/misc.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
_runInZone(() async {
|
_runInZone(() async {
|
||||||
await _initApp();
|
await _initApp();
|
||||||
@@ -120,10 +118,7 @@ void _doPlatformRelated() async {
|
|||||||
// Plus 1 to avoid 0.
|
// Plus 1 to avoid 0.
|
||||||
Computer.shared.turnOn(workersCount: (serversCount / 3).round() + 1);
|
Computer.shared.turnOn(workersCount: (serversCount / 3).round() + 1);
|
||||||
|
|
||||||
if (isIOS || isMacOS) {
|
sync.sync();
|
||||||
if (Stores.setting.icloudSync.fetch()) ICloud.sync();
|
|
||||||
}
|
|
||||||
if (Stores.setting.webdavSync.fetch()) Webdav.sync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// It may contains some async heavy funcs.
|
// It may contains some async heavy funcs.
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import 'package:computer/computer.dart';
|
|||||||
import 'package:fl_lib/fl_lib.dart';
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/core/utils/sync/icloud.dart';
|
import 'package:server_box/core/sync.dart';
|
||||||
import 'package:server_box/core/utils/sync/webdav.dart';
|
|
||||||
import 'package:server_box/data/model/app/backup.dart';
|
import 'package:server_box/data/model/app/backup.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/model/server/snippet.dart';
|
import 'package:server_box/data/model/server/snippet.dart';
|
||||||
@@ -14,10 +13,13 @@ import 'package:server_box/data/provider/snippet.dart';
|
|||||||
import 'package:server_box/data/res/misc.dart';
|
import 'package:server_box/data/res/misc.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:icons_plus/icons_plus.dart';
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
|
import 'package:server_box/data/store/no_backup.dart';
|
||||||
|
|
||||||
final icloudLoading = false.vn;
|
final icloudLoading = false.vn;
|
||||||
final webdavLoading = false.vn;
|
final webdavLoading = false.vn;
|
||||||
|
|
||||||
|
final _noBak = NoBackupStore.instance;
|
||||||
|
|
||||||
class BackupPage extends StatelessWidget {
|
class BackupPage extends StatelessWidget {
|
||||||
const BackupPage({super.key});
|
const BackupPage({super.key});
|
||||||
|
|
||||||
@@ -91,9 +93,9 @@ class BackupPage extends StatelessWidget {
|
|||||||
leading: const Icon(Icons.cloud),
|
leading: const Icon(Icons.cloud),
|
||||||
title: const Text('iCloud'),
|
title: const Text('iCloud'),
|
||||||
trailing: StoreSwitch(
|
trailing: StoreSwitch(
|
||||||
prop: Stores.setting.icloudSync,
|
prop: _noBak.icloudSync,
|
||||||
validator: (p0) {
|
validator: (p0) {
|
||||||
if (p0 && Stores.setting.webdavSync.fetch()) {
|
if (p0 && _noBak.webdavSync.fetch()) {
|
||||||
context.showSnackBar(l10n.autoBackupConflict);
|
context.showSnackBar(l10n.autoBackupConflict);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -102,7 +104,7 @@ class BackupPage extends StatelessWidget {
|
|||||||
callback: (val) async {
|
callback: (val) async {
|
||||||
if (val) {
|
if (val) {
|
||||||
icloudLoading.value = true;
|
icloudLoading.value = true;
|
||||||
await ICloud.sync();
|
await sync.sync(rs: icloud);
|
||||||
icloudLoading.value = false;
|
icloudLoading.value = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -126,17 +128,17 @@ class BackupPage extends StatelessWidget {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: Text(libL10n.auto),
|
title: Text(libL10n.auto),
|
||||||
trailing: StoreSwitch(
|
trailing: StoreSwitch(
|
||||||
prop: Stores.setting.webdavSync,
|
prop: _noBak.webdavSync,
|
||||||
validator: (p0) {
|
validator: (p0) {
|
||||||
if (p0) {
|
if (p0) {
|
||||||
if (Stores.setting.webdavUrl.fetch().isEmpty ||
|
if (_noBak.webdavUrl.fetch().isEmpty ||
|
||||||
Stores.setting.webdavUser.fetch().isEmpty ||
|
_noBak.webdavUser.fetch().isEmpty ||
|
||||||
Stores.setting.webdavPwd.fetch().isEmpty) {
|
_noBak.webdavPwd.fetch().isEmpty) {
|
||||||
context.showSnackBar(l10n.webdavSettingEmpty);
|
context.showSnackBar(l10n.webdavSettingEmpty);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Stores.setting.icloudSync.fetch()) {
|
if (_noBak.icloudSync.fetch()) {
|
||||||
context.showSnackBar(l10n.autoBackupConflict);
|
context.showSnackBar(l10n.autoBackupConflict);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -145,7 +147,7 @@ class BackupPage extends StatelessWidget {
|
|||||||
callback: (val) async {
|
callback: (val) async {
|
||||||
if (val) {
|
if (val) {
|
||||||
webdavLoading.value = true;
|
webdavLoading.value = true;
|
||||||
await Webdav.sync();
|
await sync.sync(rs: webdav);
|
||||||
webdavLoading.value = false;
|
webdavLoading.value = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -298,7 +300,7 @@ class BackupPage extends StatelessWidget {
|
|||||||
)),
|
)),
|
||||||
actions: Btn.ok(
|
actions: Btn.ok(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await backup.restore(force: true);
|
await backup.merge(force: true);
|
||||||
context.pop();
|
context.pop();
|
||||||
},
|
},
|
||||||
).toList,
|
).toList,
|
||||||
@@ -312,7 +314,7 @@ class BackupPage extends StatelessWidget {
|
|||||||
Future<void> _onTapWebdavDl(BuildContext context) async {
|
Future<void> _onTapWebdavDl(BuildContext context) async {
|
||||||
webdavLoading.value = true;
|
webdavLoading.value = true;
|
||||||
try {
|
try {
|
||||||
final files = await Webdav.list();
|
final files = await webdav.list();
|
||||||
if (files.isEmpty) return context.showSnackBar(l10n.dirEmpty);
|
if (files.isEmpty) return context.showSnackBar(l10n.dirEmpty);
|
||||||
|
|
||||||
final fileName = await context.showPickSingleDialog(
|
final fileName = await context.showPickSingleDialog(
|
||||||
@@ -321,13 +323,10 @@ class BackupPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
if (fileName == null) return;
|
if (fileName == null) return;
|
||||||
|
|
||||||
final result = await Webdav.download(relativePath: fileName);
|
await webdav.download(relativePath: fileName);
|
||||||
if (result != null) {
|
|
||||||
throw result;
|
|
||||||
}
|
|
||||||
final dlFile = await File('${Paths.doc}/$fileName').readAsString();
|
final dlFile = await File('${Paths.doc}/$fileName').readAsString();
|
||||||
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
|
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
|
||||||
await dlBak.restore(force: true);
|
await dlBak.merge(force: true);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
context.showErrDialog(e, s, libL10n.restore);
|
context.showErrDialog(e, s, libL10n.restore);
|
||||||
Loggers.app.warning('Download webdav backup failed', e, s);
|
Loggers.app.warning('Download webdav backup failed', e, s);
|
||||||
@@ -342,10 +341,7 @@ class BackupPage extends StatelessWidget {
|
|||||||
final bakName = '$date-${Miscs.bakFileName}';
|
final bakName = '$date-${Miscs.bakFileName}';
|
||||||
try {
|
try {
|
||||||
await Backup.backup(bakName);
|
await Backup.backup(bakName);
|
||||||
final uploadResult = await Webdav.upload(relativePath: bakName);
|
await webdav.upload(relativePath: bakName);
|
||||||
if (uploadResult != null) {
|
|
||||||
throw uploadResult;
|
|
||||||
}
|
|
||||||
Loggers.app.info('Upload webdav backup success');
|
Loggers.app.info('Upload webdav backup success');
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
context.showErrDialog(e, s, l10n.upload);
|
context.showErrDialog(e, s, l10n.upload);
|
||||||
@@ -356,9 +352,9 @@ class BackupPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onTapWebdavSetting(BuildContext context) async {
|
Future<void> _onTapWebdavSetting(BuildContext context) async {
|
||||||
final url = TextEditingController(text: Stores.setting.webdavUrl.fetch());
|
final url = TextEditingController(text: _noBak.webdavUrl.fetch());
|
||||||
final user = TextEditingController(text: Stores.setting.webdavUser.fetch());
|
final user = TextEditingController(text: _noBak.webdavUser.fetch());
|
||||||
final pwd = TextEditingController(text: Stores.setting.webdavPwd.fetch());
|
final pwd = TextEditingController(text: _noBak.webdavPwd.fetch());
|
||||||
final nodeUser = FocusNode();
|
final nodeUser = FocusNode();
|
||||||
final nodePwd = FocusNode();
|
final nodePwd = FocusNode();
|
||||||
final result = await context.showRoundDialog<bool>(
|
final result = await context.showRoundDialog<bool>(
|
||||||
@@ -392,13 +388,18 @@ class BackupPage extends StatelessWidget {
|
|||||||
actions: Btnx.oks,
|
actions: Btnx.oks,
|
||||||
);
|
);
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
final result = await Webdav.test(url.text, user.text, pwd.text);
|
try {
|
||||||
if (result != null) {
|
await Webdav.test(url.text, user.text, pwd.text);
|
||||||
context.showSnackBar(result);
|
context.showSnackBar(libL10n.success);
|
||||||
return;
|
webdav.init(WebdavInitArgs(
|
||||||
|
url: url.text,
|
||||||
|
user: user.text,
|
||||||
|
pwd: pwd.text,
|
||||||
|
prefix: 'serverbox/',
|
||||||
|
));
|
||||||
|
} catch (e, s) {
|
||||||
|
context.showErrDialog(e, s, 'Webdav');
|
||||||
}
|
}
|
||||||
context.showSnackBar(libL10n.success);
|
|
||||||
Webdav.changeClient(url.text, user.text, pwd.text);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +428,7 @@ class BackupPage extends StatelessWidget {
|
|||||||
)),
|
)),
|
||||||
actions: Btn.ok(
|
actions: Btn.ok(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await backup.restore(force: true);
|
await backup.merge(force: true);
|
||||||
context.pop();
|
context.pop();
|
||||||
},
|
},
|
||||||
).toList,
|
).toList,
|
||||||
|
|||||||
@@ -470,8 +470,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "v1.0.149"
|
ref: "v1.0.150"
|
||||||
resolved-ref: "291a7b445fcf116517cfbb6b3534f6b535e8276c"
|
resolved-ref: "53c92f43fff4cf643fc03e186a22268b64085e86"
|
||||||
url: "https://github.com/lppcg/fl_lib"
|
url: "https://github.com/lppcg/fl_lib"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ dependencies:
|
|||||||
fl_lib:
|
fl_lib:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/lppcg/fl_lib
|
url: https://github.com/lppcg/fl_lib
|
||||||
ref: v1.0.149
|
ref: v1.0.150
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
# dartssh2:
|
# dartssh2:
|
||||||
|
|||||||
Reference in New Issue
Block a user