优化webdav恢复

This commit is contained in:
DelLevin-Home
2026-05-22 22:42:54 +08:00
parent 6c3e55fead
commit af2ac16521
7 changed files with 348 additions and 883 deletions

View File

@@ -3,7 +3,8 @@
"allow": [ "allow": [
"Bash(flutter analyze *)", "Bash(flutter analyze *)",
"Bash(python _fix_script.py)", "Bash(python _fix_script.py)",
"Bash(dart analyze *)" "Bash(dart analyze *)",
"Bash(dart run *)"
] ]
} }
} }

View File

@@ -264,6 +264,14 @@ class _MainContentPageState extends State<MainContentPage> {
Navigator.pop(context); Navigator.pop(context);
} }
// 下载了数据则刷新界面
if (result.success && result.needReload && context.mounted) {
final provider = context.read<AppProvider>();
await provider.loadMovies();
await provider.loadBooks();
await provider.loadNotes();
}
// 显示结果 // 显示结果
if (context.mounted) { if (context.mounted) {
final isSuccess = result.success; final isSuccess = result.success;

View File

@@ -233,38 +233,12 @@ class _ProfilePageState extends State<ProfilePage> {
final notes = provider.notes; final notes = provider.notes;
final movieCount = movies.where((m) => !m.isDeleted).length; final movieCount = movies.where((m) => !m.isDeleted).length;
final watchedCount = movies.where((m) => m.status == 'watched' && !m.isDeleted).length;
final watchingCount = movies.where((m) => m.status == 'watching' && !m.isDeleted).length;
final wantToWatchCount = movies.where((m) => m.status == 'want_to_watch' && !m.isDeleted).length;
final bookCount = books.length; final bookCount = books.length;
final readCount = books.where((b) => b.status == 'read').length;
final readingCount = books.where((b) => b.status == 'reading').length;
final wantToReadCount = books.where((b) => b.status == 'want_to_read').length;
final noteCount = notes.length; final noteCount = notes.length;
final movieRatings = movies
.where((m) => m.rating != null && !m.isDeleted)
.map((m) => m.rating!);
final avgMovieRating = movieRatings.isNotEmpty
? movieRatings.reduce((a, b) => a + b) / movieRatings.length
: 0.0;
final bookRatings = books
.where((b) => b.rating != null)
.map((b) => b.rating!);
final avgBookRating = bookRatings.isNotEmpty
? bookRatings.reduce((a, b) => a + b) / bookRatings.length
: 0.0;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 24), padding: const EdgeInsets.fromLTRB(24, 16, 24, 24),
child: Column( child: Container(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 主统计卡片
Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF8F8F8), color: const Color(0xFFF8F8F8),
@@ -279,31 +253,6 @@ class _ProfilePageState extends State<ProfilePage> {
], ],
), ),
), ),
const SizedBox(height: 24),
// 详情统计
_buildSectionTitle('观影详情'),
const SizedBox(height: 12),
_buildDetailGrid([
_buildDetailItem('已看', watchedCount, Icons.check_circle_outline),
_buildDetailItem('在看', watchingCount, Icons.play_circle_outline),
_buildDetailItem('想看', wantToWatchCount, Icons.bookmark_border),
_buildDetailItem('均分', avgMovieRating > 0 ? avgMovieRating.toStringAsFixed(1) : '-', Icons.star_outline),
]),
const SizedBox(height: 20),
_buildSectionTitle('阅读详情'),
const SizedBox(height: 12),
_buildDetailGrid([
_buildDetailItem('已读', readCount, Icons.check_circle_outline),
_buildDetailItem('在读', readingCount, Icons.play_circle_outline),
_buildDetailItem('想读', wantToReadCount, Icons.bookmark_border),
_buildDetailItem('均分', avgBookRating > 0 ? avgBookRating.toStringAsFixed(1) : '-', Icons.star_outline),
]),
],
),
); );
}, },
); );
@@ -354,63 +303,6 @@ class _ProfilePageState extends State<ProfilePage> {
); );
} }
/// 详情网格
Widget _buildDetailGrid(List<Widget> children) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children,
),
);
}
/// 详情项
Widget _buildDetailItem(String label, dynamic value, IconData icon) {
return Column(
children: [
Icon(
icon,
size: 18,
color: const Color(0xFF999999),
),
const SizedBox(height: 6),
Text(
'$value',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A1A),
),
),
const SizedBox(height: 2),
Text(
label,
style: const TextStyle(
fontSize: 11,
color: Color(0xFF999999),
),
),
],
);
}
/// 区块标题
Widget _buildSectionTitle(String title) {
return Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF999999),
),
);
}
/// 功能菜单 /// 功能菜单
Widget _buildMenuSection() { Widget _buildMenuSection() {
return Container( return Container(

View File

@@ -96,6 +96,16 @@ class _StatisticsPageState extends State<StatisticsPage> {
final readingBooks = books.where((b) => b.status == 'reading').length; final readingBooks = books.where((b) => b.status == 'reading').length;
final wantToReadBooks = books.where((b) => b.status == 'want_to_read').length; final wantToReadBooks = books.where((b) => b.status == 'want_to_read').length;
final movieRatings = movies.where((m) => m.rating != null && !m.isDeleted).map((m) => m.rating!);
final avgMovieRating = movieRatings.isNotEmpty
? movieRatings.reduce((a, b) => a + b) / movieRatings.length
: null;
final bookRatings = books.where((b) => b.rating != null).map((b) => b.rating!);
final avgBookRating = bookRatings.isNotEmpty
? bookRatings.reduce((a, b) => a + b) / bookRatings.length
: null;
final children = <Widget>[]; final children = <Widget>[];
// 数据总览 // 数据总览
@@ -106,25 +116,25 @@ class _StatisticsPageState extends State<StatisticsPage> {
// 影视状态分布 // 影视状态分布
if (_showMovies) { if (_showMovies) {
children.add(_buildSectionTitle('影视状态分布')); children.add(_buildSectionTitle('影视'));
children.add(const SizedBox(height: 16)); children.add(const SizedBox(height: 16));
children.add(_buildStatusDistribution([ children.add(_buildStatusDistribution([
_StatusData('已看', watchedMovies, const Color(0xFF1A1A1A)), _StatusData('已看', watchedMovies, const Color(0xFF1A1A1A)),
_StatusData('在看', watchingMovies, const Color(0xFF666666)), _StatusData('在看', watchingMovies, const Color(0xFF666666)),
_StatusData('想看', wantToWatchMovies, const Color(0xFF999999)), _StatusData('想看', wantToWatchMovies, const Color(0xFF999999)),
])); ], avgRating: avgMovieRating));
children.add(const SizedBox(height: 32)); children.add(const SizedBox(height: 32));
} }
// 书籍状态分布 // 书籍状态分布
if (_showBooks) { if (_showBooks) {
children.add(_buildSectionTitle('书籍状态分布')); children.add(_buildSectionTitle('书籍'));
children.add(const SizedBox(height: 16)); children.add(const SizedBox(height: 16));
children.add(_buildStatusDistribution([ children.add(_buildStatusDistribution([
_StatusData('已读', readBooks, const Color(0xFF1A1A1A)), _StatusData('已读', readBooks, const Color(0xFF1A1A1A)),
_StatusData('在读', readingBooks, const Color(0xFF666666)), _StatusData('在读', readingBooks, const Color(0xFF666666)),
_StatusData('想读', wantToReadBooks, const Color(0xFF999999)), _StatusData('想读', wantToReadBooks, const Color(0xFF999999)),
])); ], avgRating: avgBookRating));
children.add(const SizedBox(height: 32)); children.add(const SizedBox(height: 32));
} }
@@ -295,7 +305,7 @@ class _StatisticsPageState extends State<StatisticsPage> {
); );
} }
Widget _buildStatusDistribution(List<_StatusData> data) { Widget _buildStatusDistribution(List<_StatusData> data, {double? avgRating}) {
final total = data.fold(0, (sum, item) => sum + item.count); final total = data.fold(0, (sum, item) => sum + item.count);
return Container( return Container(
@@ -305,7 +315,8 @@ class _StatisticsPageState extends State<StatisticsPage> {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Column( child: Column(
children: data.map((item) { children: [
...data.map((item) {
final percentage = total > 0 ? (item.count / total * 100).toStringAsFixed(1) : '0'; final percentage = total > 0 ? (item.count / total * 100).toStringAsFixed(1) : '0';
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 12),
@@ -324,7 +335,21 @@ class _StatisticsPageState extends State<StatisticsPage> {
], ],
), ),
); );
}).toList(), }),
if (avgRating != null) ...[
const Divider(height: 1, color: Color(0xFFE8E8E8)),
const SizedBox(height: 12),
Row(
children: [
const Icon(Icons.star, size: 16, color: Color(0xFF999999)),
const SizedBox(width: 10),
const Text('平均评分', style: TextStyle(fontSize: 14, color: Color(0xFF1A1A1A))),
const Spacer(),
Text(avgRating.toStringAsFixed(1), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
],
),
],
],
), ),
); );
} }

View File

@@ -524,6 +524,144 @@ class BackupService {
} }
} }
/// 从 ZIP 字节数据恢复(供 WebDAV 同步等场景使用)
Future<ImportResult> restoreFromZipBytes(Uint8List zipBytes) async {
try {
final archive = ZipDecoder().decodeBytes(zipBytes);
// 查找 data.json
final dataFile = archive.findFile('data.json');
if (dataFile == null) {
return ImportResult.error('备份文件中没有找到数据文件');
}
final jsonString = utf8.decode(dataFile.content as List<int>);
final backupData = jsonDecode(jsonString) as Map<String, dynamic>;
// 记录图片文件名到新路径的映射
final imagePathMap = <String, String>{};
int imageCount = 0;
// 解压图片到应用目录,保持目录结构
final appDir = await getApplicationDocumentsDirectory();
final imagesDir = Directory(path.join(appDir.path, 'images'));
if (!await imagesDir.exists()) {
await imagesDir.create(recursive: true);
}
for (final archiveFile in archive) {
if (archiveFile.name.startsWith('images/')) {
final relativePath = archiveFile.name.substring(7); // 去掉 'images/' 前缀
final outputFile = File(path.join(imagesDir.path, relativePath));
final parentDir = outputFile.parent;
if (!await parentDir.exists()) {
await parentDir.create(recursive: true);
}
await outputFile.writeAsBytes(archiveFile.content as List<int>);
final fileName = path.basename(archiveFile.name);
imagePathMap[fileName] = outputFile.path;
imageCount++;
}
}
// 验证备份格式
if (!backupData.containsKey('data')) {
return ImportResult.error('无效的备份文件格式');
}
// 导入数据
final data = backupData['data'] as Map<String, dynamic>;
final db = await DatabaseHelper.instance.database;
await db.transaction((txn) async {
// 清空现有数据
await txn.delete('movie_reviews');
await txn.delete('movie_posters');
await txn.delete('movies');
await txn.delete('books');
await txn.delete('notes');
if (data.containsKey('movies')) {
final movies = data['movies'] as List<dynamic>;
for (final movie in movies) {
final movieMap = _convertToDbMap(movie);
final updatedMap = _updateImagePath(movieMap, 'poster_path', imagePathMap);
await txn.insert('movies', updatedMap);
}
}
if (data.containsKey('books')) {
final books = data['books'] as List<dynamic>;
for (final book in books) {
final bookMap = _convertToDbMap(book);
final updatedMap = _updateImagePath(bookMap, 'cover_path', imagePathMap);
await txn.insert('books', updatedMap);
}
}
if (data.containsKey('notes')) {
final notes = data['notes'] as List<dynamic>;
for (final note in notes) {
final noteMap = _convertToDbMap(note);
final updatedMap = _updateNoteImagesPath(noteMap, imagePathMap);
await txn.insert('notes', updatedMap);
}
}
if (data.containsKey('movie_reviews')) {
final reviews = data['movie_reviews'] as List<dynamic>;
for (final review in reviews) {
await txn.insert('movie_reviews', _convertToDbMap(review));
}
}
if (data.containsKey('movie_posters')) {
final posters = data['movie_posters'] as List<dynamic>;
for (final poster in posters) {
final posterMap = _convertToDbMap(poster);
final updatedMap = _updateImagePath(posterMap, 'poster_path', imagePathMap);
await txn.insert('movie_posters', updatedMap);
}
}
});
// 恢复用户个人信息
if (backupData.containsKey('userInfo')) {
final userInfo = backupData['userInfo'] as Map<String, dynamic>;
final userPrefs = UserPrefs();
if (userInfo.containsKey('nickname')) {
await userPrefs.setNickname(userInfo['nickname'] as String);
}
if (userInfo.containsKey('motto')) {
await userPrefs.setMotto(userInfo['motto'] as String);
}
if (userInfo.containsKey('avatarPath')) {
final avatarPath = userInfo['avatarPath'] as String?;
if (avatarPath != null && avatarPath.isNotEmpty) {
final fileName = path.basename(avatarPath);
if (imagePathMap.containsKey(fileName)) {
await userPrefs.setAvatarPath(imagePathMap[fileName]!);
}
}
}
}
// 统计导入数量
final stats = <String, int>{};
if (data.containsKey('movies')) stats['影视'] = (data['movies'] as List).length;
if (data.containsKey('books')) stats['书籍'] = (data['books'] as List).length;
if (data.containsKey('notes')) stats['笔记'] = (data['notes'] as List).length;
if (data.containsKey('movie_reviews')) stats['影评'] = (data['movie_reviews'] as List).length;
if (data.containsKey('movie_posters')) stats['海报'] = (data['movie_posters'] as List).length;
if (imageCount > 0) stats['图片'] = imageCount;
return ImportResult.success(stats);
} catch (e) {
return ImportResult.error('恢复失败: $e');
}
}
/// 将动态类型转换为数据库可用的 Map /// 将动态类型转换为数据库可用的 Map
Map<String, dynamic> _convertToDbMap(dynamic item) { Map<String, dynamic> _convertToDbMap(dynamic item) {
if (item is Map<String, dynamic>) { if (item is Map<String, dynamic>) {

View File

@@ -1,14 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:archive/archive_io.dart'; import 'backup_service.dart';
import '../database_helper.dart';
import '../user_prefs.dart';
/// WebDAV 同步结果 /// WebDAV 同步结果
class SyncResult { class SyncResult {
@@ -40,14 +38,7 @@ enum SyncDirection {
bidirectional, // 双向同步 bidirectional, // 双向同步
} }
/// 图片同步结果 /// WebDAV 服务类 - 完整备份 zip 同步
class _ImageSyncResult {
final int uploaded;
final int downloaded;
_ImageSyncResult({required this.uploaded, required this.downloaded});
}
/// WebDAV 服务类 - 支持实时同步(数据+图片分离存储)
class WebDAVService { class WebDAVService {
static final WebDAVService _instance = WebDAVService._internal(); static final WebDAVService _instance = WebDAVService._internal();
static WebDAVService get instance => _instance; static WebDAVService get instance => _instance;
@@ -58,7 +49,6 @@ class WebDAVService {
static const String _lastSyncKey = 'webdav_last_sync'; static const String _lastSyncKey = 'webdav_last_sync';
static const String _autoSyncKey = 'webdav_auto_sync'; static const String _autoSyncKey = 'webdav_auto_sync';
static const String _autoSyncIntervalKey = 'webdav_auto_sync_interval'; static const String _autoSyncIntervalKey = 'webdav_auto_sync_interval';
static const String _lastDbModifiedKey = 'webdav_last_db_modified';
// 默认自动同步间隔(分钟) // 默认自动同步间隔(分钟)
static const int _defaultAutoSyncInterval = 5; static const int _defaultAutoSyncInterval = 5;
@@ -68,12 +58,6 @@ class WebDAVService {
bool _isAutoSyncEnabled = false; bool _isAutoSyncEnabled = false;
int _autoSyncIntervalMinutes = _defaultAutoSyncInterval; int _autoSyncIntervalMinutes = _defaultAutoSyncInterval;
// 文件系统监听
StreamSubscription<FileSystemEvent>? _imagesDirWatcher;
final Set<String> _pendingImageUploads = {};
Timer? _debounceTimer;
static const Duration _debounceDelay = Duration(seconds: 3);
/// 获取配置 /// 获取配置
Future<Map<String, String>?> getConfig() async { Future<Map<String, String>?> getConfig() async {
if (_cachedConfig != null) { if (_cachedConfig != null) {
@@ -120,7 +104,6 @@ class WebDAVService {
await prefs.remove(_lastSyncKey); await prefs.remove(_lastSyncKey);
await prefs.remove(_autoSyncKey); await prefs.remove(_autoSyncKey);
await prefs.remove(_autoSyncIntervalKey); await prefs.remove(_autoSyncIntervalKey);
await prefs.remove(_lastDbModifiedKey);
_cachedConfig = null; _cachedConfig = null;
stopAutoSync(); stopAutoSync();
} }
@@ -136,8 +119,6 @@ class WebDAVService {
final baseUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url; final baseUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url;
var davUrl = '$baseUrl$path'; var davUrl = '$baseUrl$path';
// print('WebDAV: Testing connection to $davUrl');
final client = http.Client(); final client = http.Client();
try { try {
var propfindRequest = http.Request('PROPFIND', Uri.parse(davUrl)); var propfindRequest = http.Request('PROPFIND', Uri.parse(davUrl));
@@ -151,7 +132,6 @@ class WebDAVService {
</D:propfind>'''; </D:propfind>''';
var propfindResponse = await client.send(propfindRequest); var propfindResponse = await client.send(propfindRequest);
// print('WebDAV: PROPFIND status ${propfindResponse.statusCode}');
if (propfindResponse.statusCode == 301 || if (propfindResponse.statusCode == 301 ||
propfindResponse.statusCode == 302 || propfindResponse.statusCode == 302 ||
@@ -178,12 +158,12 @@ class WebDAVService {
} else if (propfindResponse.statusCode == 401) { } else if (propfindResponse.statusCode == 401) {
return {'success': false, 'message': '认证失败,请检查用户名和密码'}; return {'success': false, 'message': '认证失败,请检查用户名和密码'};
} else if (propfindResponse.statusCode == 404) { } else if (propfindResponse.statusCode == 404) {
// print('WebDAV: Directory not found, trying to create...'); // 目录不存在,尝试创建
} else { } else {
return {'success': false, 'message': '服务器返回错误: ${propfindResponse.statusCode}'}; return {'success': false, 'message': '服务器返回错误: ${propfindResponse.statusCode}'};
} }
} catch (e) { } catch (e) {
// print('WebDAV: PROPFIND error: $e'); // ignore
} }
try { try {
@@ -191,7 +171,6 @@ class WebDAVService {
mkcolRequest.headers['Authorization'] = _basicAuth(username, password); mkcolRequest.headers['Authorization'] = _basicAuth(username, password);
final mkcolResponse = await client.send(mkcolRequest); final mkcolResponse = await client.send(mkcolRequest);
// print('WebDAV: MKCOL status ${mkcolResponse.statusCode}');
if (mkcolResponse.statusCode == 201) { if (mkcolResponse.statusCode == 201) {
return {'success': true, 'message': '连接成功,已创建目录'}; return {'success': true, 'message': '连接成功,已创建目录'};
@@ -205,18 +184,16 @@ class WebDAVService {
return {'success': false, 'message': '创建目录失败: ${mkcolResponse.statusCode}'}; return {'success': false, 'message': '创建目录失败: ${mkcolResponse.statusCode}'};
} }
} catch (e) { } catch (e) {
// print('WebDAV: MKCOL error: $e');
return {'success': false, 'message': '连接失败: $e'}; return {'success': false, 'message': '连接失败: $e'};
} finally { } finally {
client.close(); client.close();
} }
} catch (e) { } catch (e) {
// print('WebDAV test connection error: $e');
return {'success': false, 'message': '连接失败: $e'}; return {'success': false, 'message': '连接失败: $e'};
} }
} }
/// 同步数据(数据+图片分离存储,非压缩包方式) /// 同步数据 — 完整备份 zip 格式,与本地备份完全一致
Future<SyncResult> syncData({SyncDirection direction = SyncDirection.bidirectional}) async { Future<SyncResult> syncData({SyncDirection direction = SyncDirection.bidirectional}) async {
final config = await getConfig(); final config = await getConfig();
if (config == null) { if (config == null) {
@@ -229,18 +206,8 @@ class WebDAVService {
final password = config['password']!; final password = config['password']!;
final path = config['path']!; final path = config['path']!;
final dbPath = await getDatabasesPath();
final dbFile = File(p.join(dbPath, 'mooknote.db'));
if (!await dbFile.exists()) {
return SyncResult(success: false, message: '本地数据库不存在');
}
final baseUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url; final baseUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url;
final dbUrl = '$baseUrl$path/mooknote.db'; final zipUrl = '$baseUrl$path/mooknote_backup.zip';
final imagesUrl = '$baseUrl$path/images';
final avatarsUrl = '$baseUrl$path/avatars';
final userConfigUrl = '$baseUrl$path/user_config.json';
final client = http.Client(); final client = http.Client();
int uploadedFiles = 0; int uploadedFiles = 0;
@@ -251,70 +218,73 @@ class WebDAVService {
try { try {
if (direction == SyncDirection.upload) { if (direction == SyncDirection.upload) {
// 上传数据库文件 final exportResult = await BackupService.instance.exportDataForAutoBackup();
final dbSuccess = await _uploadFile(client, dbUrl, username, password, dbFile); if (!exportResult.success || exportResult.zipBytes == null) {
if (dbSuccess) { return SyncResult(success: false, message: exportResult.errorMessage ?? '创建备份失败');
uploadedFiles = 1;
} }
// 上传所有图片 final success = await _uploadBytes(client, zipUrl, username, password, exportResult.zipBytes!);
final imageResult = await _syncImages(client, imagesUrl, username, password, SyncDirection.upload); if (success) {
uploadedImages = imageResult.uploaded; uploadedFiles = 1;
uploadedImages = exportResult.imageCount;
// 上传头像 print('[WebDAV] 备份上传成功 (影视${exportResult.movieCount} 书籍${exportResult.bookCount} 笔记${exportResult.noteCount} 图片${exportResult.imageCount})');
final avatarResult = await _syncAvatars(client, avatarsUrl, username, password, SyncDirection.upload); } else {
uploadedImages += avatarResult.uploaded; return SyncResult(success: false, message: '上传备份文件失败');
}
// 上传用户配置
await _uploadUserConfig(client, userConfigUrl, username, password);
} else if (direction == SyncDirection.download) { } else if (direction == SyncDirection.download) {
// 下载数据库文件 final tempDir = await getTemporaryDirectory();
final tempDbFile = File('${dbFile.parent.path}/mooknote_download.db'); final tempZip = File(p.join(tempDir.path, 'mooknote_download.zip'));
final dbSuccess = await _downloadFile(client, dbUrl, username, password, tempDbFile); final success = await _downloadFile(client, zipUrl, username, password, tempZip);
if (dbSuccess && await tempDbFile.exists()) { if (success && await tempZip.exists()) {
// 替换本地数据库 final bytes = await tempZip.readAsBytes();
await tempDbFile.copy(dbFile.path); final importResult = await BackupService.instance.restoreFromZipBytes(bytes);
await tempDbFile.delete(); await tempZip.delete();
await DatabaseHelper.instance.reopenDatabase();
if (importResult.success) {
downloadedFiles = 1; downloadedFiles = 1;
downloadedImages = importResult.stats?['图片'] ?? 0;
needReload = true; needReload = true;
print('[WebDAV] 备份恢复成功: ${importResult.statsText}');
} else {
return SyncResult(success: false, message: importResult.errorMessage ?? '恢复备份失败');
}
} else {
return SyncResult(success: false, message: '服务器上没有备份文件,请先从其他设备上传');
} }
// 下载所有图片
final imageResult = await _syncImages(client, imagesUrl, username, password, SyncDirection.download);
downloadedImages = imageResult.downloaded;
// 下载头像
final avatarResult = await _syncAvatars(client, avatarsUrl, username, password, SyncDirection.download);
downloadedImages += avatarResult.downloaded;
// 下载并恢复用户配置
await _downloadUserConfig(client, userConfigUrl, username, password);
} else if (direction == SyncDirection.bidirectional) { } else if (direction == SyncDirection.bidirectional) {
// 双向同步:分别同步数据库和图片 final tempDir = await getTemporaryDirectory();
final dbResult = await _syncDatabaseFile(client, dbUrl, username, password, dbFile); final tempZip = File(p.join(tempDir.path, 'mooknote_bidir.zip'));
if (dbResult['uploaded'] == true) uploadedFiles = 1;
if (dbResult['downloaded'] == true) { final downloadSuccess = await _downloadFile(client, zipUrl, username, password, tempZip);
if (downloadSuccess && await tempZip.exists()) {
final bytes = await tempZip.readAsBytes();
final importResult = await BackupService.instance.restoreFromZipBytes(bytes);
await tempZip.delete();
if (importResult.success) {
downloadedFiles = 1; downloadedFiles = 1;
downloadedImages = importResult.stats?['图片'] ?? 0;
needReload = true; needReload = true;
print('[WebDAV] 远程备份已恢复: ${importResult.statsText}');
}
} else {
try { await tempZip.delete(); } catch (_) {}
print('[WebDAV] 服务器无备份,仅上传本地数据');
} }
// 双向同步图片 // 始终上传本地,确保远程是最新的
final imageResult = await _syncImagesBidirectional(client, imagesUrl, username, password); final exportResult = await BackupService.instance.exportDataForAutoBackup();
uploadedImages = imageResult.uploaded; if (exportResult.success && exportResult.zipBytes != null) {
downloadedImages = imageResult.downloaded; final uploadSuccess = await _uploadBytes(client, zipUrl, username, password, exportResult.zipBytes!);
if (uploadSuccess) {
// 双向同步头像 uploadedFiles = 1;
final avatarResult = await _syncAvatarsBidirectional(client, avatarsUrl, username, password); uploadedImages = exportResult.imageCount;
uploadedImages += avatarResult.uploaded; print('[WebDAV] 本地备份已上传 (影视${exportResult.movieCount} 书籍${exportResult.bookCount} 笔记${exportResult.noteCount} 图片${exportResult.imageCount})');
downloadedImages += avatarResult.downloaded; }
}
// 双向同步用户配置:先下载远程,再上传本地
await _downloadUserConfig(client, userConfigUrl, username, password);
await _uploadUserConfig(client, userConfigUrl, username, password);
} }
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@@ -338,271 +308,6 @@ class WebDAVService {
} }
} }
/// 同步数据库文件(双向)
Future<Map<String, bool>> _syncDatabaseFile(
http.Client client,
String dbUrl,
String username,
String password,
File localDbFile,
) async {
final result = <String, bool>{};
try {
final remoteInfo = await _getRemoteFileInfo(client, dbUrl, username, password);
final localModified = await localDbFile.lastModified();
if (remoteInfo == null) {
// 远程不存在,上传本地
result['uploaded'] = await _uploadFile(client, dbUrl, username, password, localDbFile);
} else {
final remoteModified = remoteInfo['modified'] as DateTime;
final timeDiff = localModified.difference(remoteModified).inSeconds;
if (timeDiff > 10) {
// 本地较新,上传
result['uploaded'] = await _uploadFile(client, dbUrl, username, password, localDbFile);
} else if (timeDiff < -10) {
// 远程较新,下载
final tempFile = File('${localDbFile.parent.path}/mooknote_temp.db');
final success = await _downloadFile(client, dbUrl, username, password, tempFile);
if (success) {
await tempFile.copy(localDbFile.path);
await tempFile.delete();
await DatabaseHelper.instance.reopenDatabase();
result['downloaded'] = true;
}
}
}
} catch (e) {
// 忽略错误
}
return result;
}
/// 双向同步图片(下载远程 zip 合并 -> 打包上传本地)
Future<_ImageSyncResult> _syncImagesBidirectional(
http.Client client,
String imagesUrl,
String username,
String password,
) async {
int uploaded = 0;
int downloaded = 0;
try {
final zipUrl = '${imagesUrl.substring(0, imagesUrl.lastIndexOf('/'))}/images.zip';
final tempDir = await getTemporaryDirectory();
final tempZip = File(p.join(tempDir.path, 'images_bidir.zip'));
final downloadSuccess = await _downloadFile(client, zipUrl, username, password, tempZip);
if (downloadSuccess) {
await _extractImagesZip(tempZip);
downloaded = 1;
try { await tempZip.delete(); } catch (_) {}
}
final zipFile = await _createImagesZip();
if (await zipFile.exists()) {
final uploadSuccess = await _uploadFile(client, zipUrl, username, password, zipFile);
if (uploadSuccess) uploaded = 1;
}
try { await zipFile.delete(); } catch (_) {}
} catch (e) {
// ignore
}
return _ImageSyncResult(uploaded: uploaded, downloaded: downloaded);
}
/// 同步头像目录zip 打包传输)
Future<_ImageSyncResult> _syncAvatars(
http.Client client,
String avatarsUrl,
String username,
String password,
SyncDirection direction,
) async {
int uploaded = 0;
int downloaded = 0;
try {
final zipUrl = '${avatarsUrl.substring(0, avatarsUrl.lastIndexOf('/'))}/avatars.zip';
if (direction == SyncDirection.upload) {
final zipFile = await _createAvatarsZip();
if (await zipFile.exists()) {
final success = await _uploadFile(client, zipUrl, username, password, zipFile);
if (success) uploaded = 1;
}
try { await zipFile.delete(); } catch (_) {}
} else if (direction == SyncDirection.download) {
final tempDir = await getTemporaryDirectory();
final tempZip = File(p.join(tempDir.path, 'avatars_dl.zip'));
final success = await _downloadFile(client, zipUrl, username, password, tempZip);
if (success) {
await _extractAvatarsZip(tempZip);
downloaded = 1;
}
try { await tempZip.delete(); } catch (_) {}
}
} catch (e) {
// ignore
}
return _ImageSyncResult(uploaded: uploaded, downloaded: downloaded);
}
/// 双向同步头像目录(下载远程 zip 合并 -> 打包上传本地)
Future<_ImageSyncResult> _syncAvatarsBidirectional(
http.Client client,
String avatarsUrl,
String username,
String password,
) async {
int uploaded = 0;
int downloaded = 0;
try {
final zipUrl = '${avatarsUrl.substring(0, avatarsUrl.lastIndexOf('/'))}/avatars.zip';
final tempDir = await getTemporaryDirectory();
final tempZip = File(p.join(tempDir.path, 'avatars_bidir.zip'));
final downloadSuccess = await _downloadFile(client, zipUrl, username, password, tempZip);
if (downloadSuccess) {
await _extractAvatarsZip(tempZip);
downloaded = 1;
try { await tempZip.delete(); } catch (_) {}
}
final zipFile = await _createAvatarsZip();
if (await zipFile.exists()) {
final uploadSuccess = await _uploadFile(client, zipUrl, username, password, zipFile);
if (uploadSuccess) uploaded = 1;
}
try { await zipFile.delete(); } catch (_) {}
} catch (e) {
// ignore
}
return _ImageSyncResult(uploaded: uploaded, downloaded: downloaded);
}
/// 上传用户配置
Future<void> _uploadUserConfig(
http.Client client,
String userConfigUrl,
String username,
String password,
) async {
try {
final userPrefs = UserPrefs();
final config = {
'nickname': userPrefs.nickname,
'motto': userPrefs.motto,
'avatarPath': userPrefs.avatarPath,
'updatedAt': DateTime.now().toIso8601String(),
};
final request = http.Request('PUT', Uri.parse(userConfigUrl));
request.headers['Authorization'] = _basicAuth(username, password);
request.headers['Content-Type'] = 'application/json';
request.body = jsonEncode(config);
await client.send(request);
} catch (e) {
// 忽略错误
}
}
/// 下载并恢复用户配置
Future<void> _downloadUserConfig(
http.Client client,
String userConfigUrl,
String username,
String password,
) async {
try {
final request = http.Request('GET', Uri.parse(userConfigUrl));
request.headers['Authorization'] = _basicAuth(username, password);
final response = await client.send(request);
if (response.statusCode == 200) {
final body = await response.stream.bytesToString();
final config = jsonDecode(body) as Map<String, dynamic>;
final userPrefs = UserPrefs();
if (config.containsKey('nickname')) {
await userPrefs.setNickname(config['nickname'] as String);
}
if (config.containsKey('motto')) {
await userPrefs.setMotto(config['motto'] as String);
}
if (config.containsKey('avatarPath')) {
final avatarPath = config['avatarPath'] as String?;
if (avatarPath != null && avatarPath.isNotEmpty) {
final fileName = p.basename(avatarPath);
final appDir = await getApplicationDocumentsDirectory();
final newAvatarPath = p.join(appDir.path, 'avatars', fileName);
if (await File(newAvatarPath).exists()) {
await userPrefs.setAvatarPath(newAvatarPath);
}
}
}
}
} catch (e) {
// 忽略错误
}
}
/// 获取远程文件信息
Future<Map<String, dynamic>?> _getRemoteFileInfo(
http.Client client,
String url,
String username,
String password,
) async {
try {
var request = http.Request('PROPFIND', Uri.parse(url));
request.headers['Authorization'] = _basicAuth(username, password);
request.headers['Depth'] = '0';
var response = await client.send(request);
if (response.statusCode == 301 || response.statusCode == 302 ||
response.statusCode == 307 || response.statusCode == 308) {
final location = response.headers['location'];
if (location != null) {
url = location;
request = http.Request('PROPFIND', Uri.parse(url));
request.headers['Authorization'] = _basicAuth(username, password);
request.headers['Depth'] = '0';
response = await client.send(request);
}
}
if (response.statusCode == 207) {
final body = await response.stream.bytesToString();
final modifiedMatch = RegExp(r'<d:getlastmodified>([^<]+)</d:getlastmodified>', caseSensitive: false)
.firstMatch(body);
if (modifiedMatch != null) {
final modifiedStr = modifiedMatch.group(1)!;
final modified = HttpDate.parse(modifiedStr);
return {'modified': modified, 'url': url};
}
return {'modified': DateTime.now(), 'url': url};
}
return null;
} catch (e) {
// print('WebDAV: Get remote file info error: $e');
return null;
}
}
/// 获取自动同步间隔(分钟) /// 获取自动同步间隔(分钟)
Future<int> getAutoSyncInterval() async { Future<int> getAutoSyncInterval() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@@ -618,15 +323,13 @@ class WebDAVService {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_autoSyncIntervalKey, minutes); await prefs.setInt(_autoSyncIntervalKey, minutes);
// 如果正在自动同步,重启以应用新间隔
if (_isAutoSyncEnabled) { if (_isAutoSyncEnabled) {
await startAutoSync(); await startAutoSync();
} }
} }
/// 启动自动同步(带文件监听) /// 启动自动同步
Future<void> startAutoSync() async { Future<void> startAutoSync() async {
// 停止现有的定时器和监听
await stopAutoSync(); await stopAutoSync();
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@@ -635,20 +338,18 @@ class WebDAVService {
await prefs.setBool(_autoSyncKey, true); await prefs.setBool(_autoSyncKey, true);
// 立即执行一次同步 // 立即执行一次同步
await _performIncrementalSync(); print('[WebDAV] 自动同步已启动,间隔 $_autoSyncIntervalMinutes 分钟');
await syncData(direction: SyncDirection.bidirectional);
// 设置定时器进行定期同步 // 设置定时器
_autoSyncTimer = Timer.periodic( _autoSyncTimer = Timer.periodic(
Duration(minutes: _autoSyncIntervalMinutes), Duration(minutes: _autoSyncIntervalMinutes),
(timer) async { (timer) async {
if (_isAutoSyncEnabled) { if (_isAutoSyncEnabled) {
await _performIncrementalSync(); await syncData(direction: SyncDirection.bidirectional);
} }
}, },
); );
// 启动文件系统监听
await _startFileWatcher();
} }
/// 停止自动同步 /// 停止自动同步
@@ -657,12 +358,6 @@ class WebDAVService {
_autoSyncTimer = null; _autoSyncTimer = null;
_isAutoSyncEnabled = false; _isAutoSyncEnabled = false;
// 停止文件监听
await _imagesDirWatcher?.cancel();
_imagesDirWatcher = null;
_debounceTimer?.cancel();
_pendingImageUploads.clear();
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_autoSyncKey, false); await prefs.setBool(_autoSyncKey, false);
} }
@@ -677,316 +372,25 @@ class WebDAVService {
return prefs.getBool(_autoSyncKey) ?? false; return prefs.getBool(_autoSyncKey) ?? false;
} }
/// 启动文件系统监听 /// 获取上次同步时间
Future<void> _startFileWatcher() async { Future<String?> getLastSyncTime() async {
try {
final appDir = await getApplicationDocumentsDirectory();
final imagesDir = Directory('${appDir.path}/images');
if (!await imagesDir.exists()) {
await imagesDir.create(recursive: true);
}
// 监听图片目录的变化
_imagesDirWatcher = imagesDir.watch(recursive: true).listen((event) {
if (event is FileSystemCreateEvent || event is FileSystemModifyEvent) {
final path = event.path;
if (_isImageFile(path)) {
_pendingImageUploads.add(path);
_debounceUpload();
}
}
});
} catch (e) {
// 文件监听可能不支持某些平台,忽略错误
}
}
/// 检查是否是图片文件
bool _isImageFile(String path) {
final ext = p.extension(path).toLowerCase();
return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'].contains(ext);
}
/// 防抖上传
void _debounceUpload() {
_debounceTimer?.cancel();
_debounceTimer = Timer(_debounceDelay, () async {
if (_pendingImageUploads.isNotEmpty && _isAutoSyncEnabled) {
await _uploadPendingImages();
}
});
}
/// 上传待处理的图片(重新打包 zip 上传)
Future<void> _uploadPendingImages() async {
final config = await getConfig();
if (config == null) return;
try {
final url = config['url']!;
final username = config['username']!;
final password = config['password']!;
final path = config['path']!;
final baseUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url;
final zipUrl = '$baseUrl$path/images.zip';
_pendingImageUploads.clear();
final client = http.Client();
try {
final zipFile = await _createImagesZip();
if (await zipFile.exists()) {
await _uploadFile(client, zipUrl, username, password, zipFile);
}
try { await zipFile.delete(); } catch (_) {}
} finally {
client.close();
}
} catch (e) {
// 忽略错误
}
}
/// 执行增量同步(检查变更并上传)
Future<SyncResult> _performIncrementalSync() async {
final config = await getConfig();
if (config == null) {
return SyncResult(success: false, message: '未配置 WebDAV');
}
try {
final url = config['url']!;
final username = config['username']!;
final password = config['password']!;
final path = config['path']!;
final dbPath = await getDatabasesPath();
final dbFile = File(p.join(dbPath, 'mooknote.db'));
if (!await dbFile.exists()) {
return SyncResult(success: false, message: '本地数据库不存在');
}
final baseUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url;
final dbUrl = '$baseUrl$path/mooknote.db';
final imagesUrl = '$baseUrl$path/images';
final avatarsUrl = '$baseUrl$path/avatars';
final userConfigUrl = '$baseUrl$path/user_config.json';
final client = http.Client();
int uploadedImages = 0;
int downloadedImages = 0;
bool dbUploaded = false;
try {
// 检查数据库是否需要同步
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final lastDbModifiedStr = prefs.getString(_lastDbModifiedKey); return prefs.getString(_lastSyncKey);
final currentDbModified = await dbFile.lastModified();
bool needDbSync = true;
if (lastDbModifiedStr != null) {
final lastDbModified = DateTime.parse(lastDbModifiedStr);
// 如果数据库修改时间在3秒内认为没有变化
if (currentDbModified.difference(lastDbModified).inSeconds.abs() < 3) {
needDbSync = false;
}
} }
if (needDbSync) { /// 上传字节数据到 WebDAV
// 上传数据库 Future<bool> _uploadBytes(
dbUploaded = await _uploadFile(client, dbUrl, username, password, dbFile);
if (dbUploaded) {
await prefs.setString(_lastDbModifiedKey, currentDbModified.toIso8601String());
}
}
// 同步图片(双向)
final imageResult = await _syncImagesBidirectional(client, imagesUrl, username, password);
uploadedImages = imageResult.uploaded;
downloadedImages = imageResult.downloaded;
// 同步头像(双向)
final avatarResult = await _syncAvatarsBidirectional(client, avatarsUrl, username, password);
uploadedImages += avatarResult.uploaded;
downloadedImages += avatarResult.downloaded;
// 同步用户配置:下载远程并上传本地
await _downloadUserConfig(client, userConfigUrl, username, password);
await _uploadUserConfig(client, userConfigUrl, username, password);
await prefs.setString(_lastSyncKey, DateTime.now().toIso8601String());
return SyncResult(
success: true,
message: '自动同步完成',
lastSyncTime: DateTime.now(),
uploadedFiles: dbUploaded ? 1 : 0,
uploadedImages: uploadedImages,
downloadedImages: downloadedImages,
);
} finally {
client.close();
}
} catch (e) {
return SyncResult(success: false, message: '自动同步失败: $e');
}
}
/// 将本地 images 目录打包为 zip 文件,返回临时文件
Future<File> _createImagesZip() async {
final appDir = await getApplicationDocumentsDirectory();
final imagesDir = Directory(p.join(appDir.path, 'images'));
final tempDir = await getTemporaryDirectory();
final zipFile = File(p.join(tempDir.path, 'images.zip'));
final archive = Archive();
if (await imagesDir.exists()) {
await for (final entity in imagesDir.list(recursive: true)) {
if (entity is File) {
final bytes = await entity.readAsBytes();
final relativePath = p.relative(entity.path, from: imagesDir.path);
archive.addFile(ArchiveFile(relativePath, bytes.length, bytes));
}
}
}
final zipBytes = ZipEncoder().encode(archive)!;
await zipFile.writeAsBytes(zipBytes);
return zipFile;
}
/// 解压 images.zip 到本地 images 目录(合并模式)
Future<void> _extractImagesZip(File zipFile) async {
final appDir = await getApplicationDocumentsDirectory();
final imagesDir = Directory(p.join(appDir.path, 'images'));
final inputStream = InputFileStream(zipFile.path);
final archive = ZipDecoder().decodeBuffer(inputStream);
await inputStream.close();
for (final file in archive) {
if (file.isFile) {
final targetFile = File(p.join(imagesDir.path, file.name));
await targetFile.parent.create(recursive: true);
await targetFile.writeAsBytes(file.content!);
}
}
}
/// 将本地 avatars 目录打包为 zip 文件
Future<File> _createAvatarsZip() async {
final appDir = await getApplicationDocumentsDirectory();
final avatarsDir = Directory(p.join(appDir.path, 'avatars'));
final tempDir = await getTemporaryDirectory();
final zipFile = File(p.join(tempDir.path, 'avatars.zip'));
final archive = Archive();
if (await avatarsDir.exists()) {
await for (final entity in avatarsDir.list(recursive: true)) {
if (entity is File) {
final bytes = await entity.readAsBytes();
final relativePath = p.relative(entity.path, from: avatarsDir.path);
archive.addFile(ArchiveFile(relativePath, bytes.length, bytes));
}
}
}
final zipBytes = ZipEncoder().encode(archive)!;
await zipFile.writeAsBytes(zipBytes);
return zipFile;
}
/// 解压 avatars.zip 到本地 avatars 目录(合并模式)
Future<void> _extractAvatarsZip(File zipFile) async {
final appDir = await getApplicationDocumentsDirectory();
final avatarsDir = Directory(p.join(appDir.path, 'avatars'));
final inputStream = InputFileStream(zipFile.path);
final archive = ZipDecoder().decodeBuffer(inputStream);
await inputStream.close();
for (final file in archive) {
if (file.isFile) {
final targetFile = File(p.join(avatarsDir.path, file.name));
await targetFile.parent.create(recursive: true);
await targetFile.writeAsBytes(file.content!);
}
}
}
/// 同步图片zip 打包传输,一次请求完成)
Future<_ImageSyncResult> _syncImages(
http.Client client,
String imagesUrl,
String username,
String password,
SyncDirection direction,
) async {
int uploaded = 0;
int downloaded = 0;
try {
final zipUrl = '${imagesUrl.substring(0, imagesUrl.lastIndexOf('/'))}/images.zip';
if (direction == SyncDirection.upload) {
final zipFile = await _createImagesZip();
if (await zipFile.exists()) {
final success = await _uploadFile(client, zipUrl, username, password, zipFile);
if (success) uploaded = 1;
}
try { await zipFile.delete(); } catch (_) {}
} else if (direction == SyncDirection.download) {
final tempDir = await getTemporaryDirectory();
final tempZip = File(p.join(tempDir.path, 'images_dl.zip'));
final success = await _downloadFile(client, zipUrl, username, password, tempZip);
if (success) {
await _extractImagesZip(tempZip);
downloaded = 1;
}
try { await tempZip.delete(); } catch (_) {}
}
} catch (e) {
// ignore
}
return _ImageSyncResult(uploaded: uploaded, downloaded: downloaded);
}
/// 上传文件(数据库文件上传前自动 VACUUM 压缩)
Future<bool> _uploadFile(
http.Client client, http.Client client,
String url, String url,
String username, String username,
String password, String password,
File file, Uint8List bytes,
) async { ) async {
try { try {
// 数据库文件:上传前 VACUUM 压缩
if (p.basename(url).endsWith('.db')) {
try {
final db = await DatabaseHelper.instance.database;
await db.execute('VACUUM');
} catch (_) {}
}
final fileBytes = await file.readAsBytes();
var request = http.Request('PUT', Uri.parse(url)); var request = http.Request('PUT', Uri.parse(url));
request.headers['Authorization'] = _basicAuth(username, password); request.headers['Authorization'] = _basicAuth(username, password);
request.headers['Content-Type'] = 'application/octet-stream'; request.headers['Content-Type'] = 'application/zip';
request.bodyBytes = fileBytes; request.bodyBytes = bytes;
var response = await client.send(request); var response = await client.send(request);
@@ -997,20 +401,21 @@ class WebDAVService {
if (location != null) { if (location != null) {
request = http.Request('PUT', Uri.parse(location)); request = http.Request('PUT', Uri.parse(location));
request.headers['Authorization'] = _basicAuth(username, password); request.headers['Authorization'] = _basicAuth(username, password);
request.headers['Content-Type'] = 'application/octet-stream'; request.headers['Content-Type'] = 'application/zip';
request.bodyBytes = fileBytes; request.bodyBytes = bytes;
response = await client.send(request); response = await client.send(request);
} }
} }
return response.statusCode == 201 || response.statusCode == 204; print('[WebDAV] PUT $url -> ${response.statusCode}');
return response.statusCode == 200 || response.statusCode == 201 || response.statusCode == 204;
} catch (e) { } catch (e) {
// print('WebDAV: Upload error: $e'); print('[WebDAV] _uploadBytes error: $e');
return false; return false;
} }
} }
/// 下载文件 /// 下载文件到本地
Future<bool> _downloadFile( Future<bool> _downloadFile(
http.Client client, http.Client client,
String url, String url,
@@ -1019,8 +424,6 @@ class WebDAVService {
File localFile, File localFile,
) async { ) async {
try { try {
// print('WebDAV: Downloading from $url to ${localFile.path}');
var request = http.Request('GET', Uri.parse(url)); var request = http.Request('GET', Uri.parse(url));
request.headers['Authorization'] = _basicAuth(username, password); request.headers['Authorization'] = _basicAuth(username, password);
@@ -1031,26 +434,24 @@ class WebDAVService {
response.statusCode == 307 || response.statusCode == 308) { response.statusCode == 307 || response.statusCode == 308) {
final location = response.headers['location']; final location = response.headers['location'];
if (location != null) { if (location != null) {
// print('WebDAV: Following redirect to $location');
request = http.Request('GET', Uri.parse(location)); request = http.Request('GET', Uri.parse(location));
request.headers['Authorization'] = _basicAuth(username, password); request.headers['Authorization'] = _basicAuth(username, password);
response = await client.send(request); response = await client.send(request);
} }
} }
// print('WebDAV: Download response status: ${response.statusCode}'); print('[WebDAV] GET $url -> ${response.statusCode}');
if (response.statusCode == 200) { if (response.statusCode == 200) {
await localFile.parent.create(recursive: true);
final bytes = await response.stream.toBytes(); final bytes = await response.stream.toBytes();
await localFile.writeAsBytes(bytes); await localFile.writeAsBytes(bytes);
// print('WebDAV: Downloaded ${bytes.length} bytes to ${localFile.path}'); print('[WebDAV] Downloaded ${bytes.length} bytes');
return true; return true;
} else {
// print('WebDAV: Download failed with status ${response.statusCode}');
} }
return false; return false;
} catch (e) { } catch (e) {
// print('WebDAV: Download error: $e'); // ignore
return false; return false;
} }
} }

View File

@@ -1,7 +1,7 @@
name: mooknote name: mooknote
description: "app for tracking movies, books, and notes" description: "app for tracking movies, books, and notes"
publish_to: 'none' publish_to: 'none'
version: 0.1.7+2 version: 0.1.8
environment: environment:
sdk: ^3.5.0 sdk: ^3.5.0