import 'dart:io'; import 'package:archive/archive_io.dart'; import 'package:cloud_archiver/config.dart'; import 'package:cloud_archiver/state.dart'; import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:webdav_client/webdav_client.dart' as dav; late dav.Client _client; String totalSize() { if (cloudArchives.value.isEmpty) return '0 MB'; return filesize( cloudArchives.value.values .map((archives) => archives.map((archive) => archive.size ?? 0).reduce((total, size) => total + size)) .reduce((total, size) => total + size), 1, ); } Future connect(String server, String user, String auth) async { var client = dav.newClient( server, debug: true, user: user, password: auth, ); client.setHeaders({'accept-charset': 'utf-8'}); client.setConnectTimeout(5 * 1000); // Set send data timeout time in milliseconds. client.setSendTimeout(60 * 1000); // Set transfer data time in milliseconds. client.setReceiveTimeout(60 * 1000); try { davStatus.value = DavStatus.loading; await client.ping(); davStatus.value = DavStatus.connected; _client = client; return true; } catch (e) { print('$e'); davStatus.value = DavStatus.notConnected; } return false; } void loadLocalStatus() { if (appConfig?.syncItems.isNotEmpty != true) return; var result = {}; for (var syncItem in appConfig!.syncItems) { var file = File(syncItem.path); var stat = file.statSync(); switch (stat.type) { case FileSystemEntityType.file: result[syncItem] = stat.modified; case FileSystemEntityType.directory: result[syncItem] = dirModifiedTime(syncItem.path); default: result[syncItem] = null; } } localSyncItems.value = { ...localSyncItems.value, ...result, }; } DateTime dirModifiedTime(String dir) { var directory = Directory(dir); var modified = directory.statSync().modified; for (var entry in Directory(dir).listSync(recursive: true)) { var stat = entry.statSync(); if (stat.type == FileSystemEntityType.file) { if (stat.modified.isAfter(modified)) { modified = stat.modified; } } } return modified; } void loadCloudArchives() async { if (davStatus.value != DavStatus.connected) return; davStatus.value = DavStatus.loading; List list = []; try { list = await _client.readDir('/'); } catch (e) { print(e); davStatus.value = DavStatus.notConnected; return; } var result = >{}; for (var item in list) { if (item.isDir == true && item.path?.isNotEmpty == true) { var files = await _client.readDir(item.path!); result[basename(item.path!)] = files; } } cloudArchives.value = result; davStatus.value = DavStatus.connected; } Future upload(BuildContext context, SyncItemConfig item, bool needCreateDir) async { if (needCreateDir) { await _client.mkdir(item.name); } var file = File(item.path); var cacheDir = await getApplicationCacheDirectory(); var zipFileEncoder = ZipFileEncoder(); var zipFileName = '${appConfig?.client ?? 'DEBUG'}-${DateTime.now().strFileName}.zip'; var zipTempFilePath = join(cacheDir.path, zipFileName); zipFileEncoder.create(zipTempFilePath); if (file.statSync().type == FileSystemEntityType.directory) { var directory = Directory(item.path); for (var file in directory.listSync()) { print('Add file: ${file.path}'); } await zipFileEncoder.addDirectory( directory, onProgress: (progress) { print('Zip progress: $progress'); }, ); } else { await zipFileEncoder.addFile(file); } await zipFileEncoder.close(); await _client.writeFromFile(zipTempFilePath, join(item.name, zipFileName)); File(zipTempFilePath).delete(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('上传完成'), ), ); } loadCloudArchives(); } Future download(BuildContext context, SyncItemConfig item, dav.File cloudArchive) async { print('Start download: ${cloudArchive.path}'); var cacheDir = await getApplicationCacheDirectory(); var zipPath = join(cacheDir.path, '${item.name}_${cloudArchive.name}'); await _client.read2File( cloudArchive.path!, zipPath, onProgress: (count, total) { print('Downloaded: $count/$total'); }, ); await extractFileToDisk( zipPath, cacheDir.path, callback: (entry) { print('Unzip: ${entry.name}'); }, ); File(zipPath).delete(); var temp = File(join(cacheDir.path, basename(item.path))); print('Temp path: ${temp.path}'); var fileSystemEntityType = temp.statSync().type; switch (fileSystemEntityType) { case FileSystemEntityType.directory: var directory = Directory(item.path); if (directory.existsSync()) { directory.deleteSync(recursive: true); } await Directory(temp.path).safeMoveDirectory(item.path); case FileSystemEntityType.file: var file = File(item.path); if (file.existsSync()) { file.deleteSync(); } await temp.safeMoveFile(item.path); default: print('Failed: $fileSystemEntityType'); } loadLocalStatus(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('下载完成'), ), ); } } extension DateTimeExt on DateTime { String get strHm => '$year/${padZero(month)}/${padZero(day)} ${padZero(hour)}:${padZero(minute)}'; String get strHms => '$strHm:${padZero(second)}'; String get strFileName => '${year}_${padZero(month)}_${padZero(day)}_${padZero(hour)}_${padZero(minute)}_${padZero(second)}'; String padZero(int num, [fix = 2]) => num.toString().padLeft(fix, '0'); } /// 安全地移动文件和目录(支持跨分区) extension on File { Future safeMoveFile(String destPath) async { try { await rename(destPath); return; } on FileSystemException catch (e) { // 捕获到 errno = 18 (EXDEV) 跨分区错误时,退化到“复制+删除”方案 if (e.osError?.errorCode != 18) { rethrow; // 其他错误继续抛出 } } await copy(destPath); await delete(); } } extension on Directory { Future safeMoveDirectory(String destPath) async { // 1. 如果在同一分区,优先尝试 rename,效率最高 try { await rename(destPath); return; } on FileSystemException catch (e) { // 捕获到 errno = 18 (EXDEV) 跨分区错误时,退化到“复制+删除”方案 if (e.osError?.errorCode != 18) { rethrow; // 其他错误继续抛出 } } // 2. 跨分区移动:递归复制 await _copyDirectory(this, Directory(destPath)); // 3. 复制完成后删除源目录 await delete(recursive: true); } } /// 递归复制目录 Future _copyDirectory(Directory source, Directory destination) async { // 确保目标目录存在 await destination.create(recursive: true); await for (var entity in source.list(recursive: false)) { // 计算在新目录下的对应路径 final newPath = '${destination.path}/${entity.uri.pathSegments.lastWhere((s) => s.isNotEmpty)}'; if (entity is File) { await entity.copy(newPath); } else if (entity is Directory) { await _copyDirectory(entity, Directory(newPath)); } } }