import 'dart:async'; import 'dart:io'; import 'package:archive/archive.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import 'log/logger.dart'; /// GZIP文件管理器 /// 用于创建和分享GZIP压缩文件,不需要密码保护 class GzipManager { static const String TAG = 'GzipManager'; static final GzipManager _instance = GzipManager._internal(); factory GzipManager() => _instance; GzipManager._internal(); // 防重复点击机制 DateTime? _lastGenerateTime; String? _lastGeneratedFilePath; static const Duration _throttleDuration = Duration(minutes: 1); /// 生成GZIP压缩文件并分享 /// /// [filePaths] 要压缩的文件路径列表 /// [gzipFileName] gzip文件名,默认为 "nomo_logs_时间戳.tar.gz" /// [deleteExisting] 是否删除已存在的同名文件,默认为true /// /// 返回生成的gzip文件路径,如果失败返回null Future generateAndShareGzip({ required List filePaths, String? gzipFileName, bool deleteExisting = true, }) async { try { // 检查防重复点击 if (_shouldUseLastGeneratedFile()) { if (_lastGeneratedFilePath != null && await File(_lastGeneratedFilePath!).exists()) { // await _shareFile(_lastGeneratedFilePath!); return _lastGeneratedFilePath; } } // 验证文件路径 final validFilePaths = await _validateFilePaths(filePaths); if (validFilePaths.isEmpty) { throw Exception('no valid file paths'); } // 生成gzip文件 final gzipFilePath = await _generateGzip( filePaths: validFilePaths, gzipFileName: gzipFileName, deleteExisting: deleteExisting, ); if (gzipFilePath != null) { // 更新防重复点击状态 _lastGenerateTime = DateTime.now(); _lastGeneratedFilePath = gzipFilePath; // 分享文件 // await _shareFile(gzipFilePath); return gzipFilePath; } return null; } catch (e) { log(TAG, 'generate gzip file failed: $e'); return null; } } /// 检查是否应该使用上次生成的文件 bool _shouldUseLastGeneratedFile() { if (_lastGenerateTime == null) return false; final timeDiff = DateTime.now().difference(_lastGenerateTime!); return timeDiff < _throttleDuration; } /// 验证文件路径 Future> _validateFilePaths(List filePaths) async { final validPaths = []; for (final filePath in filePaths) { final file = File(filePath); if (await file.exists()) { validPaths.add(filePath); } else { log(TAG, 'file not exists: $filePath'); } } return validPaths; } /// 生成GZIP压缩文件 Future _generateGzip({ required List filePaths, String? gzipFileName, bool deleteExisting = true, }) async { try { // 获取临时目录 final tempDir = await getTemporaryDirectory(); // 生成gzip文件名 final timestamp = DateTime.now().millisecondsSinceEpoch; final fileName = gzipFileName ?? 'nomo_logs_$timestamp.tar.gz'; final gzipFilePath = path.join(tempDir.path, fileName); // 删除已存在的文件 if (deleteExisting) { final existingFile = File(gzipFilePath); if (await existingFile.exists()) { await existingFile.delete(); } } // 创建GZIP压缩文件 return await _createGzipFile( filePaths: filePaths, gzipFilePath: gzipFilePath, ); } catch (e) { log(TAG, 'generate gzip file failed: $e'); return null; } } /// 创建GZIP压缩文件 Future _createGzipFile({ required List filePaths, required String gzipFilePath, }) async { try { // 创建TAR归档 final tarArchive = Archive(); // 添加文件到TAR归档 for (final filePath in filePaths) { final file = File(filePath); final fileName = path.basename(filePath); final fileBytes = await file.readAsBytes(); // 创建TAR文件条目 final tarFile = ArchiveFile(fileName, fileBytes.length, fileBytes); tarArchive.addFile(tarFile); } // 将TAR归档转换为字节 final tarData = TarEncoder().encode(tarArchive); // 使用GZIP压缩TAR数据 final gzipData = GZipEncoder().encode(tarData); final gzipFile = File(gzipFilePath); await gzipFile.writeAsBytes(gzipData); log(TAG, 'gzip file generated successfully: $gzipFilePath'); return gzipFilePath; } catch (e) { log(TAG, 'create gzip file failed: $e'); rethrow; } } /// 解压GZIP文件 /// /// [gzipFilePath] gzip文件路径 /// [outputDir] 输出目录,默认为临时目录 Future> extractGzip({ required String gzipFilePath, String? outputDir, }) async { try { final gzipFile = File(gzipFilePath); if (!await gzipFile.exists()) { throw Exception('gzip文件不存在: $gzipFilePath'); } // 读取gzip文件 var gzipBytes = await gzipFile.readAsBytes(); log(TAG, 'read gzip file size: ${gzipBytes.length} bytes'); // 解压GZIP log(TAG, 'start to decompress gzip file...'); final tarBytes = GZipDecoder().decodeBytes(gzipBytes); log( TAG, 'gzip decompressed successfully, tar size: ${tarBytes.length} bytes', ); // 解压TAR log(TAG, 'start to extract tar archive...'); final archive = TarDecoder().decodeBytes(tarBytes); log(TAG, 'tar extracted successfully, contains ${archive.length} files'); // 确定输出目录 final outputDirectory = outputDir ?? (await getTemporaryDirectory()).path; final extractedFiles = []; // 提取文件 for (final file in archive) { if (file.isFile) { // 写入解压后的文件 final outputPath = path.join(outputDirectory, file.name); final outputFile = File(outputPath); await outputFile.writeAsBytes(file.content); extractedFiles.add(outputPath); log(TAG, 'extract file: ${file.name} -> $outputPath'); } } log(TAG, 'successfully extracted ${extractedFiles.length} files'); return extractedFiles; } catch (e) { log(TAG, 'extract gzip file failed: $e'); log(TAG, 'error type: ${e.runtimeType}'); rethrow; } } /// 清除缓存的文件 Future clearCache() async { try { if (_lastGeneratedFilePath != null) { final file = File(_lastGeneratedFilePath!); if (await file.exists()) { await file.delete(); } } _lastGeneratedFilePath = null; _lastGenerateTime = null; } catch (e) { log(TAG, 'clear cache failed: $e'); } } /// 获取上次生成的文件路径 String? get lastGeneratedFilePath => _lastGeneratedFilePath; /// 获取上次生成的时间 DateTime? get lastGenerateTime => _lastGenerateTime; /// 检查是否在防重复点击时间窗口内 bool get isInThrottleWindow => _shouldUseLastGeneratedFile(); /// 获取文件大小信息 Future> getFileInfo(String filePath) async { try { final file = File(filePath); if (await file.exists()) { final stat = await file.stat(); return { 'path': filePath, 'name': path.basename(filePath), 'size': stat.size, 'modified': stat.modified, 'created': stat.changed, }; } return {}; } catch (e) { log(TAG, 'get file info failed: $e'); return {}; } } /// 批量获取文件信息 Future>> getFilesInfo( List filePaths, ) async { final filesInfo = >[]; for (final filePath in filePaths) { final info = await getFileInfo(filePath); if (info.isNotEmpty) { filesInfo.add(info); } } return filesInfo; } /// 检查文件是否为GZIP格式 Future isGzipFile(String filePath) async { try { final file = File(filePath); if (!await file.exists()) { return false; } // 读取文件头部字节 final bytes = await file.openRead(0, 2).first; if (bytes.length < 2) { return false; } // GZIP文件头标识:0x1f 0x8b return bytes[0] == 0x1f && bytes[1] == 0x8b; } catch (e) { log(TAG, 'check gzip file format failed: $e'); return false; } } /// 获取GZIP文件中的文件列表 Future> getGzipFileList(String gzipFilePath) async { try { final gzipFile = File(gzipFilePath); if (!await gzipFile.exists()) { throw Exception('gzip file not exists: $gzipFilePath'); } // 读取并解压GZIP var gzipBytes = await gzipFile.readAsBytes(); final tarBytes = GZipDecoder().decodeBytes(gzipBytes); // 解析TAR归档 final archive = TarDecoder().decodeBytes(tarBytes); final fileList = []; for (final file in archive) { if (file.isFile) { fileList.add(file.name); } } return fileList; } catch (e) { log(TAG, 'get gzip file list failed: $e'); rethrow; } } /// 获取压缩比信息 Future> getCompressionInfo(String gzipFilePath) async { try { final gzipFile = File(gzipFilePath); if (!await gzipFile.exists()) { return {}; } final gzipSize = await gzipFile.length(); // 解压获取原始大小 var gzipBytes = await gzipFile.readAsBytes(); final tarBytes = GZipDecoder().decodeBytes(gzipBytes); final originalSize = tarBytes.length; final compressionRatio = originalSize > 0 ? ((1 - gzipSize / originalSize) * 100).toStringAsFixed(2) : '0.00'; return { 'originalSize': originalSize, 'compressedSize': gzipSize, 'compressionRatio': '$compressionRatio%', 'spaceSaved': originalSize - gzipSize, }; } catch (e) { log(TAG, 'get compression info failed: $e'); return {}; } } }