mirror of
https://github.com/ZhuJHua/moodiary.git
synced 2026-04-05 16:31:45 +08:00
feat: add an abstract sync interface
This commit is contained in:
11
lib/common/models/sync/sync.dart
Normal file
11
lib/common/models/sync/sync.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:moodiary/common/values/sync_status.dart';
|
||||
|
||||
class SyncResult<T> {
|
||||
final SyncStatus status;
|
||||
T? data;
|
||||
|
||||
SyncResult({
|
||||
required this.status,
|
||||
this.data,
|
||||
});
|
||||
}
|
||||
32
lib/common/values/sync_status.dart
Normal file
32
lib/common/values/sync_status.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
/// 同步状态枚举
|
||||
enum SyncStatus {
|
||||
/// 已分配任务但还未执行
|
||||
pending,
|
||||
|
||||
/// 正在同步中
|
||||
syncing,
|
||||
|
||||
/// 同步成功
|
||||
success,
|
||||
|
||||
/// 同步失败
|
||||
failure,
|
||||
|
||||
/// 验证失败
|
||||
invalid,
|
||||
|
||||
/// 未知状态
|
||||
unknown,
|
||||
}
|
||||
|
||||
/// 连接状态枚举
|
||||
enum ConnectivityStatus {
|
||||
/// 未连接
|
||||
disconnected,
|
||||
|
||||
/// 正在连接
|
||||
connecting,
|
||||
|
||||
/// 已连接
|
||||
connected,
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moodiary/common/values/webdav.dart';
|
||||
import 'package:moodiary/presentation/pref.dart';
|
||||
import 'package:moodiary/presentation/secure_storage.dart';
|
||||
import 'package:moodiary/utils/notice_util.dart';
|
||||
import 'package:moodiary/utils/webdav_util.dart';
|
||||
import 'package:refreshed/refreshed.dart';
|
||||
@@ -27,6 +28,7 @@ class WebDavLogic extends GetxController {
|
||||
if (state.hasOption.value) {
|
||||
await checkConnectivity();
|
||||
}
|
||||
await checkHasUserKey();
|
||||
super.onReady();
|
||||
}
|
||||
|
||||
@@ -41,6 +43,11 @@ class WebDavLogic extends GetxController {
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
Future<void> checkHasUserKey() async {
|
||||
state.hasUserKey.value =
|
||||
(await SecureStorageUtil.getValue('userKey')) != null;
|
||||
}
|
||||
|
||||
Future<void> checkConnectivity() async {
|
||||
state.connectivityStatus.value = WebDavConnectivityStatus.connecting;
|
||||
final res = await webDav.checkConnectivity();
|
||||
@@ -111,4 +118,9 @@ class WebDavLogic extends GetxController {
|
||||
await PrefUtil.setValue<bool>('autoSyncAfterChange', value);
|
||||
state.autoSyncAfterChange.value = value;
|
||||
}
|
||||
|
||||
void setSyncEncryption(bool value) async {
|
||||
await PrefUtil.setValue<bool>('syncEncryption', value);
|
||||
state.syncEncryption.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,5 +18,8 @@ class WebDavState {
|
||||
RxBool autoSyncAfterChange =
|
||||
PrefUtil.getValue<bool>('autoSyncAfterChange')!.obs;
|
||||
|
||||
RxBool syncEncryption = PrefUtil.getValue<bool>('syncEncryption')!.obs;
|
||||
RxBool hasUserKey = false.obs;
|
||||
|
||||
WebDavState();
|
||||
}
|
||||
|
||||
@@ -59,6 +59,14 @@ class WebDavComponent extends StatelessWidget {
|
||||
subtitle: l10n.webdavSyncAfterChangeDes,
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
return AdaptiveSwitchListTile(
|
||||
value: state.syncEncryption.value,
|
||||
onChanged: state.hasUserKey.value ? logic.setSyncEncryption : null,
|
||||
title: Text(l10n.webdavSyncEncryption),
|
||||
subtitle: l10n.webdavSyncEncryptionDes,
|
||||
);
|
||||
}),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
|
||||
108
lib/services/sync/impl/minio_impl.dart
Normal file
108
lib/services/sync/impl/minio_impl.dart
Normal file
@@ -0,0 +1,108 @@
|
||||
// import 'dart:ui';
|
||||
//
|
||||
// import 'package:minio/minio.dart';
|
||||
// import 'package:moodiary/common/models/isar/diary.dart';
|
||||
// import 'package:moodiary/common/models/sync/sync.dart';
|
||||
// import 'package:moodiary/services/sync/sync.dart';
|
||||
//
|
||||
// class MinioSyncServiceImpl implements SyncService {
|
||||
// final String endPoint;
|
||||
//
|
||||
// final String accessKey;
|
||||
//
|
||||
// final String secretKey;
|
||||
//
|
||||
// final String bucketName;
|
||||
//
|
||||
// late final Minio _client;
|
||||
//
|
||||
// MinioSyncServiceImpl({
|
||||
// required this.endPoint,
|
||||
// required this.accessKey,
|
||||
// required this.secretKey,
|
||||
// required this.bucketName,
|
||||
// });
|
||||
//
|
||||
// @override
|
||||
// Future<void> init() async {
|
||||
// _client = Minio(
|
||||
// endPoint: endPoint,
|
||||
// accessKey: accessKey,
|
||||
// secretKey: secretKey,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Future<bool> checkConnectivity() {
|
||||
// // TODO: implement checkConnectivity
|
||||
// throw UnimplementedError();
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Future<SyncResult<Map<String, String>>> fetchServerSyncData() {
|
||||
// // TODO: implement fetchServerSyncData
|
||||
// throw UnimplementedError();
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Future<SyncResult> updateServerSyncData(Map<String, String> syncData) {
|
||||
// // TODO: implement updateServerSyncData
|
||||
// throw UnimplementedError();
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Future<SyncResult> deleteDiary({
|
||||
// required Diary diary,
|
||||
// VoidCallback? onStart,
|
||||
// VoidCallback? onComplete,
|
||||
// Function(int p1, int p2)? onProgress,
|
||||
// }) {
|
||||
// // TODO: implement deleteDiary
|
||||
// throw UnimplementedError();
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Future<SyncResult> downloadDiary({
|
||||
// required String diaryId,
|
||||
// VoidCallback? onStart,
|
||||
// VoidCallback? onComplete,
|
||||
// Function(int p1, int p2)? onProgress,
|
||||
// }) {
|
||||
// // TODO: implement downloadDiary
|
||||
// throw UnimplementedError();
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Future<SyncResult> updateDiary({
|
||||
// required Diary oldDiary,
|
||||
// required Diary newDiary,
|
||||
// VoidCallback? onStart,
|
||||
// VoidCallback? onComplete,
|
||||
// Function(int p1, int p2)? onProgress,
|
||||
// }) {
|
||||
// // TODO: implement updateDiary
|
||||
// throw UnimplementedError();
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Future<SyncResult> uploadDiary({
|
||||
// required Diary diary,
|
||||
// VoidCallback? onStart,
|
||||
// VoidCallback? onComplete,
|
||||
// Function(int p1, int p2)? onProgress,
|
||||
// }) {
|
||||
// // TODO: implement uploadDiary
|
||||
// throw UnimplementedError();
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Future<SyncResult> syncDiary({
|
||||
// required List<Diary> diaries,
|
||||
// VoidCallback? onStart,
|
||||
// VoidCallback? onComplete,
|
||||
// Function(int p1, int p2)? onProgress,
|
||||
// }) {
|
||||
// // TODO: implement syncDiary
|
||||
// throw UnimplementedError();
|
||||
// }
|
||||
// }
|
||||
266
lib/services/sync/impl/webdav_impl.dart
Normal file
266
lib/services/sync/impl/webdav_impl.dart
Normal file
@@ -0,0 +1,266 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moodiary/common/models/isar/diary.dart';
|
||||
import 'package:moodiary/common/models/sync/sync.dart';
|
||||
import 'package:moodiary/common/values/sync_status.dart';
|
||||
import 'package:moodiary/common/values/webdav.dart';
|
||||
import 'package:moodiary/services/sync/sync.dart';
|
||||
import 'package:moodiary/utils/log_util.dart';
|
||||
import 'package:refreshed/refreshed.dart';
|
||||
import 'package:webdav_client/webdav_client.dart' as webdav;
|
||||
|
||||
class WebdavSyncServiceImpl implements SyncService {
|
||||
final String url;
|
||||
final String username;
|
||||
final String password;
|
||||
|
||||
late final webdav.Client _client;
|
||||
final Rx<ConnectivityStatus> _connectivity =
|
||||
ConnectivityStatus.connecting.obs;
|
||||
|
||||
bool get _isConnected => _connectivity.value == ConnectivityStatus.connected;
|
||||
late Map<String, String> _syncStatus;
|
||||
Timer? _connectivityTimer;
|
||||
|
||||
WebdavSyncServiceImpl({
|
||||
required this.url,
|
||||
required this.username,
|
||||
required this.password,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
_client = webdav.newClient(
|
||||
url,
|
||||
user: username,
|
||||
password: password,
|
||||
debug: false,
|
||||
);
|
||||
await checkConnectivity();
|
||||
startPolling();
|
||||
}
|
||||
|
||||
void startPolling({Duration interval = const Duration(minutes: 1)}) {
|
||||
_connectivityTimer?.cancel();
|
||||
_connectivityTimer = Timer.periodic(interval, (timer) async {
|
||||
await checkConnectivity();
|
||||
});
|
||||
}
|
||||
|
||||
void stopPolling() {
|
||||
_connectivityTimer?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> checkConnectivity() async {
|
||||
_connectivity.value = ConnectivityStatus.connecting;
|
||||
try {
|
||||
await _client.ping().timeout(const Duration(seconds: 3));
|
||||
_connectivity.value = ConnectivityStatus.connected;
|
||||
} catch (e) {
|
||||
LogUtil.printError("WebDAV Error Check Connectivity", error: e);
|
||||
_connectivity.value = ConnectivityStatus.disconnected;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initDir() async {
|
||||
if (!_isConnected) return;
|
||||
|
||||
final paths = [
|
||||
WebDavOptions.imagePath,
|
||||
WebDavOptions.videoPath,
|
||||
WebDavOptions.audioPath,
|
||||
WebDavOptions.diaryPath,
|
||||
WebDavOptions.categoryPath,
|
||||
];
|
||||
|
||||
for (final path in paths) {
|
||||
try {
|
||||
await _client.mkdirAll(path);
|
||||
} catch (e) {
|
||||
LogUtil.printError("创建目录失败: $path", error: e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await _client.read(WebDavOptions.syncFlagPath);
|
||||
} catch (_) {
|
||||
await _client.write(
|
||||
WebDavOptions.syncFlagPath,
|
||||
utf8.encode(jsonEncode({})),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SyncResult<Map<String, String>>> fetchServerSyncData() async {
|
||||
if (!_isConnected) return SyncResult(status: SyncStatus.invalid);
|
||||
|
||||
try {
|
||||
final response = await _client.read(WebDavOptions.syncFlagPath);
|
||||
if (response.isNotEmpty) {
|
||||
return SyncResult(
|
||||
status: SyncStatus.success,
|
||||
data: jsonDecode(utf8.decode(response)) as Map<String, String>,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
LogUtil.printError("获取服务器同步数据失败", error: e);
|
||||
}
|
||||
return SyncResult(status: SyncStatus.failure);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SyncResult<bool>> updateServerSyncData(
|
||||
Map<String, String> syncData,
|
||||
) async {
|
||||
if (!_isConnected) return SyncResult(status: SyncStatus.invalid);
|
||||
|
||||
try {
|
||||
await _client.write(
|
||||
WebDavOptions.syncFlagPath,
|
||||
utf8.encode(jsonEncode(syncData)),
|
||||
);
|
||||
return SyncResult(status: SyncStatus.success, data: true);
|
||||
} catch (e) {
|
||||
LogUtil.printError("更新服务器同步数据失败", error: e);
|
||||
return SyncResult(status: SyncStatus.failure);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SyncResult> deleteDiary({
|
||||
required Diary diary,
|
||||
VoidCallback? onStart,
|
||||
VoidCallback? onComplete,
|
||||
Function(int p1, int p2)? onProgress,
|
||||
}) async {
|
||||
if (!_isConnected) return SyncResult(status: SyncStatus.invalid);
|
||||
|
||||
if (!_syncStatus.containsKey(diary.id)) {
|
||||
return SyncResult(status: SyncStatus.success);
|
||||
}
|
||||
|
||||
_syncStatus[diary.id] = 'delete';
|
||||
await updateServerSyncData(_syncStatus);
|
||||
|
||||
// 批量删除文件
|
||||
try {
|
||||
await Future.wait([
|
||||
_deleteFile('${WebDavOptions.diaryPath}/${diary.id}.json'),
|
||||
_deleteFile('${WebDavOptions.diaryPath}/${diary.id}.bin'),
|
||||
_deleteFiles(
|
||||
diary.imageName,
|
||||
'${WebDavOptions.imagePath}/${diary.id}',
|
||||
'image',
|
||||
),
|
||||
_deleteFiles(
|
||||
diary.audioName,
|
||||
'${WebDavOptions.audioPath}/${diary.id}',
|
||||
'audio',
|
||||
),
|
||||
_deleteFiles(
|
||||
diary.videoName,
|
||||
'${WebDavOptions.videoPath}/${diary.id}',
|
||||
'video',
|
||||
),
|
||||
_deleteFiles(
|
||||
diary.videoName
|
||||
.map(
|
||||
(videoName) => 'thumbnail-${videoName.substring(6, 42)}.jpeg',
|
||||
)
|
||||
.toList(),
|
||||
'${WebDavOptions.videoPath}/${diary.id}',
|
||||
'thumbnail',
|
||||
),
|
||||
_deleteFile('${WebDavOptions.imagePath}/${diary.id}'),
|
||||
_deleteFile('${WebDavOptions.audioPath}/${diary.id}'),
|
||||
_deleteFile('${WebDavOptions.videoPath}/${diary.id}'),
|
||||
]);
|
||||
} catch (e) {
|
||||
LogUtil.printError("删除日记失败", error: e);
|
||||
return SyncResult(status: SyncStatus.failure);
|
||||
}
|
||||
|
||||
return SyncResult(status: SyncStatus.success);
|
||||
}
|
||||
|
||||
Future<void> _deleteFile(String path) async {
|
||||
try {
|
||||
await _client.remove(path);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteFiles(
|
||||
List<String> fileNames,
|
||||
String resourcePath,
|
||||
String type,
|
||||
) async {
|
||||
for (final fileName in fileNames) {
|
||||
await _deleteFile('$resourcePath/$fileName');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SyncResult> downloadDiary({
|
||||
required String diaryId,
|
||||
VoidCallback? onStart,
|
||||
VoidCallback? onComplete,
|
||||
Function(int p1, int p2)? onProgress,
|
||||
}) async {
|
||||
if (!_isConnected) return SyncResult(status: SyncStatus.invalid);
|
||||
debugPrint("下载日记 $diaryId (未实现)");
|
||||
return SyncResult(status: SyncStatus.failure);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SyncResult> uploadDiary({
|
||||
required Diary diary,
|
||||
VoidCallback? onStart,
|
||||
VoidCallback? onComplete,
|
||||
Function(int p1, int p2)? onProgress,
|
||||
}) async {
|
||||
if (!_isConnected) return SyncResult(status: SyncStatus.invalid);
|
||||
debugPrint("上传日记 ${diary.id} (未实现)");
|
||||
return SyncResult(status: SyncStatus.failure);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
stopPolling();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SyncResult> updateDiary({
|
||||
required Diary oldDiary,
|
||||
required Diary newDiary,
|
||||
VoidCallback? onStart,
|
||||
VoidCallback? onComplete,
|
||||
Function(int p1, int p2)? onProgress,
|
||||
}) {
|
||||
// TODO: implement updateDiary
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SyncResult> syncDiary({
|
||||
required List<Diary> diaries,
|
||||
VoidCallback? onUpload,
|
||||
VoidCallback? onDownload,
|
||||
VoidCallback? onComplete,
|
||||
Function(int p1, int p2)? onProgress,
|
||||
}) async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement hasConfig
|
||||
bool get hasConfig => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
// TODO: implement connectivity
|
||||
ConnectivityStatus get rxConnectivity => _connectivity.value;
|
||||
}
|
||||
153
lib/services/sync/sync.dart
Normal file
153
lib/services/sync/sync.dart
Normal file
@@ -0,0 +1,153 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moodiary/common/models/isar/diary.dart';
|
||||
import 'package:moodiary/common/models/sync/sync.dart';
|
||||
import 'package:moodiary/common/values/sync_status.dart';
|
||||
import 'package:moodiary/services/sync/impl/webdav_impl.dart';
|
||||
import 'package:refreshed/refreshed.dart';
|
||||
|
||||
/// 同步服务的抽象基类
|
||||
abstract class SyncService {
|
||||
/// 是否有配置
|
||||
bool get hasConfig;
|
||||
|
||||
/// 连通性
|
||||
/// 返回值为 [ConnectivityStatus]
|
||||
/// 这是一个响应式变量,可以通过 [Obx] 进行监听
|
||||
ConnectivityStatus get rxConnectivity;
|
||||
|
||||
/// 初始化方法
|
||||
Future<void> init();
|
||||
|
||||
/// 连通性检查
|
||||
Future<void> checkConnectivity();
|
||||
|
||||
/// 同步日记
|
||||
/// 这个方法会自动进行增量同步
|
||||
Future<SyncResult> syncDiary({
|
||||
required List<Diary> diaries,
|
||||
VoidCallback? onUpload,
|
||||
VoidCallback? onDownload,
|
||||
VoidCallback? onComplete,
|
||||
Function(int, int)? onProgress,
|
||||
});
|
||||
|
||||
/// 上传日记
|
||||
/// 可选参数为加密
|
||||
/// 返回值 [SyncResult] 为同步结果
|
||||
/// [onStart] 为开始回调
|
||||
/// [onComplete] 为完成回调
|
||||
/// [onProgress] 为进度回调
|
||||
Future<SyncResult> uploadDiary({
|
||||
required Diary diary,
|
||||
VoidCallback? onStart,
|
||||
VoidCallback? onComplete,
|
||||
Function(int, int)? onProgress,
|
||||
});
|
||||
|
||||
/// 更新日记
|
||||
/// 可选参数为加密
|
||||
/// 返回值 [SyncResult] 为同步结果
|
||||
/// [onStart] 为开始回调
|
||||
/// [onComplete] 为完成回调
|
||||
/// [onProgress] 为进度回调
|
||||
Future<SyncResult> updateDiary({
|
||||
required Diary oldDiary,
|
||||
required Diary newDiary,
|
||||
VoidCallback? onStart,
|
||||
VoidCallback? onComplete,
|
||||
Function(int, int)? onProgress,
|
||||
});
|
||||
|
||||
/// 下载日记
|
||||
/// 返回值 [SyncResult] 为同步结果
|
||||
/// [onStart] 为开始回调
|
||||
/// [onComplete] 为完成回调
|
||||
/// [onProgress] 为进度回调
|
||||
Future<SyncResult> downloadDiary({
|
||||
required String diaryId,
|
||||
VoidCallback? onStart,
|
||||
VoidCallback? onComplete,
|
||||
Function(int, int)? onProgress,
|
||||
});
|
||||
|
||||
/// 删除日记
|
||||
/// 返回值 [SyncResult] 为同步结果
|
||||
/// [onStart] 为开始回调
|
||||
/// [onComplete] 为完成回调
|
||||
/// [onProgress] 为进度回调
|
||||
Future<SyncResult> deleteDiary({
|
||||
required Diary diary,
|
||||
VoidCallback? onStart,
|
||||
VoidCallback? onComplete,
|
||||
Function(int, int)? onProgress,
|
||||
});
|
||||
|
||||
/// 获取服务器同步数据
|
||||
/// 文件名为 [sync.json]
|
||||
/// 返回值 [SyncResult] 为同步结果
|
||||
/// [Map] 为同步数据
|
||||
Future<SyncResult<Map<String, String>>> fetchServerSyncData();
|
||||
|
||||
/// 更新服务器同步数据
|
||||
Future<SyncResult> updateServerSyncData(Map<String, String> syncData);
|
||||
}
|
||||
|
||||
/// 同步服务的具体实现
|
||||
/// 通过 [WebdavSyncServiceImpl] 进行同步
|
||||
class WebdavSyncService extends GetxService {
|
||||
late final SyncService _webdavSyncService;
|
||||
|
||||
Future<void> init({
|
||||
required String baseUrl,
|
||||
required String username,
|
||||
required String password,
|
||||
}) async {
|
||||
_webdavSyncService = WebdavSyncServiceImpl(
|
||||
url: baseUrl,
|
||||
username: username,
|
||||
password: password,
|
||||
);
|
||||
await _webdavSyncService.init();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
// TODO: implement onInit
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// TODO: implement onClose
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
// TODO: implement onReady
|
||||
super.onReady();
|
||||
}
|
||||
}
|
||||
|
||||
// /// 通过 MinIO 对象存储进行同步
|
||||
// /// 通过 [MinioSyncServiceImpl] 进行同步
|
||||
// class MinioSyncService extends GetxService {
|
||||
// late final SyncService _minioSyncService;
|
||||
//
|
||||
// Future<void> init({
|
||||
// required String endPoint,
|
||||
// required String accessKey,
|
||||
// required String secretKey,
|
||||
// required String bucketName,
|
||||
// }) async {
|
||||
// _minioSyncService = MinioSyncServiceImpl(
|
||||
// endPoint: endPoint,
|
||||
// accessKey: accessKey,
|
||||
// secretKey: secretKey,
|
||||
// bucketName: bucketName,
|
||||
// );
|
||||
// await _minioSyncService.init();
|
||||
// }
|
||||
// }
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart' as flutter;
|
||||
import 'package:moodiary/common/models/isar/category.dart';
|
||||
@@ -9,6 +10,8 @@ import 'package:moodiary/common/values/webdav.dart';
|
||||
import 'package:moodiary/pages/home/diary/diary_logic.dart';
|
||||
import 'package:moodiary/presentation/isar.dart';
|
||||
import 'package:moodiary/presentation/pref.dart';
|
||||
import 'package:moodiary/presentation/secure_storage.dart';
|
||||
import 'package:moodiary/utils/aes_util.dart';
|
||||
import 'package:moodiary/utils/file_util.dart';
|
||||
import 'package:moodiary/utils/log_util.dart';
|
||||
import 'package:refreshed/refreshed.dart';
|
||||
@@ -40,7 +43,6 @@ class WebDavUtil {
|
||||
_client = null;
|
||||
}
|
||||
// 尝试连接,如果失败,
|
||||
|
||||
try {
|
||||
_client = webdav.newClient(
|
||||
webDavOption[0],
|
||||
@@ -134,6 +136,7 @@ class WebDavUtil {
|
||||
await updateServerSyncData(serverSyncData);
|
||||
// 删除日记json
|
||||
await _client!.remove('${WebDavOptions.diaryPath}/${diary.id}.json');
|
||||
await _client!.remove('${WebDavOptions.diaryPath}/${diary.id}.bin');
|
||||
// 遍历删除日记资源文件
|
||||
await _deleteFiles(
|
||||
diary.imageName, '${WebDavOptions.imagePath}/${diary.id}', 'image');
|
||||
@@ -341,7 +344,30 @@ class WebDavUtil {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _checkShouldEncrypt() async {
|
||||
return PrefUtil.getValue<bool>('syncEncryption') == true &&
|
||||
(await SecureStorageUtil.getValue('userKey')) != null;
|
||||
}
|
||||
|
||||
Future<void> _uploadDiary(Diary diary) async {
|
||||
Uint8List diaryData;
|
||||
String diaryPath;
|
||||
// 检查有没有开启加密
|
||||
final shouldEncrypt = await _checkShouldEncrypt();
|
||||
if (shouldEncrypt) {
|
||||
// 尝试获取用户密钥
|
||||
final userKey = await SecureStorageUtil.getValue('userKey');
|
||||
// 生成加密密钥, 用日记 ID 和用户密钥生成
|
||||
final key = await AesUtil.deriveKey(salt: diary.id, userKey: userKey!);
|
||||
// 加密日记内容
|
||||
diaryPath = '${WebDavOptions.diaryPath}/${diary.id}.bin';
|
||||
diaryData =
|
||||
await AesUtil.encrypt(key: key, data: jsonEncode(diary.toJson()));
|
||||
} else {
|
||||
diaryPath = '${WebDavOptions.diaryPath}/${diary.id}.json';
|
||||
diaryData = utf8.encode(jsonEncode(diary.toJson()));
|
||||
}
|
||||
|
||||
// 检查并上传分类
|
||||
if (diary.categoryId != null) {
|
||||
final categoryName =
|
||||
@@ -350,20 +376,16 @@ class WebDavUtil {
|
||||
await _uploadCategory(diary.categoryId!, categoryName);
|
||||
}
|
||||
}
|
||||
|
||||
// 上传日记 JSON 数据
|
||||
final diaryPath = '${WebDavOptions.diaryPath}/${diary.id}.json';
|
||||
final diaryData = jsonEncode(diary.toJson());
|
||||
LogUtil.printInfo(diaryData);
|
||||
try {
|
||||
_client!.setHeaders({
|
||||
'accept-charset': 'utf-8',
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Type':
|
||||
shouldEncrypt ? 'application/octet-stream' : 'application/json',
|
||||
});
|
||||
await _client!.write(diaryPath, utf8.encode(diaryData));
|
||||
LogUtil.printInfo('Diary JSON uploaded: $diaryPath');
|
||||
await _client!.write(diaryPath, diaryData);
|
||||
LogUtil.printInfo('Diary uploaded: $diaryPath');
|
||||
} catch (e) {
|
||||
LogUtil.printInfo('Failed to upload diary JSON: $e');
|
||||
LogUtil.printInfo('Failed to upload diary : $e');
|
||||
rethrow;
|
||||
}
|
||||
|
||||
@@ -422,17 +444,43 @@ class WebDavUtil {
|
||||
|
||||
Future<Diary> _downloadDiary(String diaryId) async {
|
||||
// 下载日记 JSON 数据
|
||||
final diaryPath = '${WebDavOptions.diaryPath}/$diaryId.json';
|
||||
final normalDiaryPath = '${WebDavOptions.diaryPath}/$diaryId.json';
|
||||
final encryptedDiaryPath = '${WebDavOptions.diaryPath}/$diaryId.bin';
|
||||
late Diary diary;
|
||||
|
||||
try {
|
||||
final diaryData = await _client!.read(diaryPath);
|
||||
diary = await flutter.compute(Diary.fromJson,
|
||||
jsonDecode(utf8.decode(diaryData)) as Map<String, dynamic>);
|
||||
LogUtil.printInfo('Diary JSON downloaded: $diaryPath');
|
||||
// 先尝试普通 JSON 格式
|
||||
try {
|
||||
final diaryData = await _client!.read(normalDiaryPath);
|
||||
diary = await flutter.compute(Diary.fromJson,
|
||||
jsonDecode(utf8.decode(diaryData)) as Map<String, dynamic>);
|
||||
LogUtil.printInfo('Diary JSON downloaded: $normalDiaryPath');
|
||||
} catch (e) {
|
||||
LogUtil.printInfo('Failed to download normal JSON: $e');
|
||||
// 再尝试二进制格式
|
||||
try {
|
||||
final encryptedDiaryData = await _client!.read(encryptedDiaryPath);
|
||||
// 解密日记内容
|
||||
final userKey = await SecureStorageUtil.getValue('userKey');
|
||||
final shouldEncrypt = await _checkShouldEncrypt();
|
||||
if (!shouldEncrypt) {
|
||||
throw Exception('User key not found or encryption not enabled');
|
||||
}
|
||||
final key = await AesUtil.deriveKey(salt: diaryId, userKey: userKey!);
|
||||
final decryptedData = await AesUtil.decrypt(
|
||||
key: key,
|
||||
encryptedData: Uint8List.fromList(encryptedDiaryData),
|
||||
);
|
||||
diary = await flutter.compute(Diary.fromJson,
|
||||
jsonDecode(decryptedData) as Map<String, dynamic>);
|
||||
LogUtil.printInfo('Diary binary downloaded: $encryptedDiaryPath');
|
||||
} catch (e) {
|
||||
LogUtil.printInfo('Failed to download binary diary: $e');
|
||||
// 两种方式都失败,抛出最终异常
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
LogUtil.printInfo('Failed to download diary JSON: $e');
|
||||
rethrow;
|
||||
throw Exception('Failed to download diary: $e');
|
||||
}
|
||||
|
||||
// 同步分类
|
||||
|
||||
Reference in New Issue
Block a user