Files
cloud_archiver/client/lib/utils.dart
T

274 lines
7.5 KiB
Dart

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/foundation.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<bool> connect(String server, String user, String auth) async {
var client = dav.newClient(
server,
debug: kDebugMode,
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 = <SyncItemConfig, DateTime?>{};
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<dav.File> list = [];
try {
list = await _client.readDir('/');
} catch (e) {
print(e);
davStatus.value = DavStatus.notConnected;
return;
}
var result = <String, List<dav.File>>{};
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<void> 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<void> 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<void> 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<void> 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<void> _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));
}
}
}