gzip_manager.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:archive/archive.dart';
  4. import 'package:path_provider/path_provider.dart';
  5. import 'package:path/path.dart' as path;
  6. import 'log/logger.dart';
  7. /// GZIP文件管理器
  8. /// 用于创建和分享GZIP压缩文件,不需要密码保护
  9. class GzipManager {
  10. static const String TAG = 'GzipManager';
  11. static final GzipManager _instance = GzipManager._internal();
  12. factory GzipManager() => _instance;
  13. GzipManager._internal();
  14. // 防重复点击机制
  15. DateTime? _lastGenerateTime;
  16. String? _lastGeneratedFilePath;
  17. static const Duration _throttleDuration = Duration(minutes: 1);
  18. /// 生成GZIP压缩文件并分享
  19. ///
  20. /// [filePaths] 要压缩的文件路径列表
  21. /// [gzipFileName] gzip文件名,默认为 "nomo_logs_时间戳.tar.gz"
  22. /// [deleteExisting] 是否删除已存在的同名文件,默认为true
  23. ///
  24. /// 返回生成的gzip文件路径,如果失败返回null
  25. Future<String?> generateAndShareGzip({
  26. required List<String> filePaths,
  27. String? gzipFileName,
  28. bool deleteExisting = true,
  29. }) async {
  30. try {
  31. // 检查防重复点击
  32. if (_shouldUseLastGeneratedFile()) {
  33. if (_lastGeneratedFilePath != null &&
  34. await File(_lastGeneratedFilePath!).exists()) {
  35. // await _shareFile(_lastGeneratedFilePath!);
  36. return _lastGeneratedFilePath;
  37. }
  38. }
  39. // 验证文件路径
  40. final validFilePaths = await _validateFilePaths(filePaths);
  41. if (validFilePaths.isEmpty) {
  42. throw Exception('no valid file paths');
  43. }
  44. // 生成gzip文件
  45. final gzipFilePath = await _generateGzip(
  46. filePaths: validFilePaths,
  47. gzipFileName: gzipFileName,
  48. deleteExisting: deleteExisting,
  49. );
  50. if (gzipFilePath != null) {
  51. // 更新防重复点击状态
  52. _lastGenerateTime = DateTime.now();
  53. _lastGeneratedFilePath = gzipFilePath;
  54. // 分享文件
  55. // await _shareFile(gzipFilePath);
  56. return gzipFilePath;
  57. }
  58. return null;
  59. } catch (e) {
  60. log(TAG, 'generate gzip file failed: $e');
  61. return null;
  62. }
  63. }
  64. /// 检查是否应该使用上次生成的文件
  65. bool _shouldUseLastGeneratedFile() {
  66. if (_lastGenerateTime == null) return false;
  67. final timeDiff = DateTime.now().difference(_lastGenerateTime!);
  68. return timeDiff < _throttleDuration;
  69. }
  70. /// 验证文件路径
  71. Future<List<String>> _validateFilePaths(List<String> filePaths) async {
  72. final validPaths = <String>[];
  73. for (final filePath in filePaths) {
  74. final file = File(filePath);
  75. if (await file.exists()) {
  76. validPaths.add(filePath);
  77. } else {
  78. log(TAG, 'file not exists: $filePath');
  79. }
  80. }
  81. return validPaths;
  82. }
  83. /// 生成GZIP压缩文件
  84. Future<String?> _generateGzip({
  85. required List<String> filePaths,
  86. String? gzipFileName,
  87. bool deleteExisting = true,
  88. }) async {
  89. try {
  90. // 获取临时目录
  91. final tempDir = await getTemporaryDirectory();
  92. // 生成gzip文件名
  93. final timestamp = DateTime.now().millisecondsSinceEpoch;
  94. final fileName = gzipFileName ?? 'nomo_logs_$timestamp.tar.gz';
  95. final gzipFilePath = path.join(tempDir.path, fileName);
  96. // 删除已存在的文件
  97. if (deleteExisting) {
  98. final existingFile = File(gzipFilePath);
  99. if (await existingFile.exists()) {
  100. await existingFile.delete();
  101. }
  102. }
  103. // 创建GZIP压缩文件
  104. return await _createGzipFile(
  105. filePaths: filePaths,
  106. gzipFilePath: gzipFilePath,
  107. );
  108. } catch (e) {
  109. log(TAG, 'generate gzip file failed: $e');
  110. return null;
  111. }
  112. }
  113. /// 创建GZIP压缩文件
  114. Future<String?> _createGzipFile({
  115. required List<String> filePaths,
  116. required String gzipFilePath,
  117. }) async {
  118. try {
  119. // 创建TAR归档
  120. final tarArchive = Archive();
  121. // 添加文件到TAR归档
  122. for (final filePath in filePaths) {
  123. final file = File(filePath);
  124. final fileName = path.basename(filePath);
  125. final fileBytes = await file.readAsBytes();
  126. // 创建TAR文件条目
  127. final tarFile = ArchiveFile(fileName, fileBytes.length, fileBytes);
  128. tarArchive.addFile(tarFile);
  129. }
  130. // 将TAR归档转换为字节
  131. final tarData = TarEncoder().encode(tarArchive);
  132. // 使用GZIP压缩TAR数据
  133. final gzipData = GZipEncoder().encode(tarData);
  134. final gzipFile = File(gzipFilePath);
  135. await gzipFile.writeAsBytes(gzipData);
  136. log(TAG, 'gzip file generated successfully: $gzipFilePath');
  137. return gzipFilePath;
  138. } catch (e) {
  139. log(TAG, 'create gzip file failed: $e');
  140. rethrow;
  141. }
  142. }
  143. /// 解压GZIP文件
  144. ///
  145. /// [gzipFilePath] gzip文件路径
  146. /// [outputDir] 输出目录,默认为临时目录
  147. Future<List<String>> extractGzip({
  148. required String gzipFilePath,
  149. String? outputDir,
  150. }) async {
  151. try {
  152. final gzipFile = File(gzipFilePath);
  153. if (!await gzipFile.exists()) {
  154. throw Exception('gzip文件不存在: $gzipFilePath');
  155. }
  156. // 读取gzip文件
  157. var gzipBytes = await gzipFile.readAsBytes();
  158. log(TAG, 'read gzip file size: ${gzipBytes.length} bytes');
  159. // 解压GZIP
  160. log(TAG, 'start to decompress gzip file...');
  161. final tarBytes = GZipDecoder().decodeBytes(gzipBytes);
  162. log(
  163. TAG,
  164. 'gzip decompressed successfully, tar size: ${tarBytes.length} bytes',
  165. );
  166. // 解压TAR
  167. log(TAG, 'start to extract tar archive...');
  168. final archive = TarDecoder().decodeBytes(tarBytes);
  169. log(TAG, 'tar extracted successfully, contains ${archive.length} files');
  170. // 确定输出目录
  171. final outputDirectory = outputDir ?? (await getTemporaryDirectory()).path;
  172. final extractedFiles = <String>[];
  173. // 提取文件
  174. for (final file in archive) {
  175. if (file.isFile) {
  176. // 写入解压后的文件
  177. final outputPath = path.join(outputDirectory, file.name);
  178. final outputFile = File(outputPath);
  179. await outputFile.writeAsBytes(file.content);
  180. extractedFiles.add(outputPath);
  181. log(TAG, 'extract file: ${file.name} -> $outputPath');
  182. }
  183. }
  184. log(TAG, 'successfully extracted ${extractedFiles.length} files');
  185. return extractedFiles;
  186. } catch (e) {
  187. log(TAG, 'extract gzip file failed: $e');
  188. log(TAG, 'error type: ${e.runtimeType}');
  189. rethrow;
  190. }
  191. }
  192. /// 清除缓存的文件
  193. Future<void> clearCache() async {
  194. try {
  195. if (_lastGeneratedFilePath != null) {
  196. final file = File(_lastGeneratedFilePath!);
  197. if (await file.exists()) {
  198. await file.delete();
  199. }
  200. }
  201. _lastGeneratedFilePath = null;
  202. _lastGenerateTime = null;
  203. } catch (e) {
  204. log(TAG, 'clear cache failed: $e');
  205. }
  206. }
  207. /// 获取上次生成的文件路径
  208. String? get lastGeneratedFilePath => _lastGeneratedFilePath;
  209. /// 获取上次生成的时间
  210. DateTime? get lastGenerateTime => _lastGenerateTime;
  211. /// 检查是否在防重复点击时间窗口内
  212. bool get isInThrottleWindow => _shouldUseLastGeneratedFile();
  213. /// 获取文件大小信息
  214. Future<Map<String, dynamic>> getFileInfo(String filePath) async {
  215. try {
  216. final file = File(filePath);
  217. if (await file.exists()) {
  218. final stat = await file.stat();
  219. return {
  220. 'path': filePath,
  221. 'name': path.basename(filePath),
  222. 'size': stat.size,
  223. 'modified': stat.modified,
  224. 'created': stat.changed,
  225. };
  226. }
  227. return {};
  228. } catch (e) {
  229. log(TAG, 'get file info failed: $e');
  230. return {};
  231. }
  232. }
  233. /// 批量获取文件信息
  234. Future<List<Map<String, dynamic>>> getFilesInfo(
  235. List<String> filePaths,
  236. ) async {
  237. final filesInfo = <Map<String, dynamic>>[];
  238. for (final filePath in filePaths) {
  239. final info = await getFileInfo(filePath);
  240. if (info.isNotEmpty) {
  241. filesInfo.add(info);
  242. }
  243. }
  244. return filesInfo;
  245. }
  246. /// 检查文件是否为GZIP格式
  247. Future<bool> isGzipFile(String filePath) async {
  248. try {
  249. final file = File(filePath);
  250. if (!await file.exists()) {
  251. return false;
  252. }
  253. // 读取文件头部字节
  254. final bytes = await file.openRead(0, 2).first;
  255. if (bytes.length < 2) {
  256. return false;
  257. }
  258. // GZIP文件头标识:0x1f 0x8b
  259. return bytes[0] == 0x1f && bytes[1] == 0x8b;
  260. } catch (e) {
  261. log(TAG, 'check gzip file format failed: $e');
  262. return false;
  263. }
  264. }
  265. /// 获取GZIP文件中的文件列表
  266. Future<List<String>> getGzipFileList(String gzipFilePath) async {
  267. try {
  268. final gzipFile = File(gzipFilePath);
  269. if (!await gzipFile.exists()) {
  270. throw Exception('gzip file not exists: $gzipFilePath');
  271. }
  272. // 读取并解压GZIP
  273. var gzipBytes = await gzipFile.readAsBytes();
  274. final tarBytes = GZipDecoder().decodeBytes(gzipBytes);
  275. // 解析TAR归档
  276. final archive = TarDecoder().decodeBytes(tarBytes);
  277. final fileList = <String>[];
  278. for (final file in archive) {
  279. if (file.isFile) {
  280. fileList.add(file.name);
  281. }
  282. }
  283. return fileList;
  284. } catch (e) {
  285. log(TAG, 'get gzip file list failed: $e');
  286. rethrow;
  287. }
  288. }
  289. /// 获取压缩比信息
  290. Future<Map<String, dynamic>> getCompressionInfo(String gzipFilePath) async {
  291. try {
  292. final gzipFile = File(gzipFilePath);
  293. if (!await gzipFile.exists()) {
  294. return {};
  295. }
  296. final gzipSize = await gzipFile.length();
  297. // 解压获取原始大小
  298. var gzipBytes = await gzipFile.readAsBytes();
  299. final tarBytes = GZipDecoder().decodeBytes(gzipBytes);
  300. final originalSize = tarBytes.length;
  301. final compressionRatio = originalSize > 0
  302. ? ((1 - gzipSize / originalSize) * 100).toStringAsFixed(2)
  303. : '0.00';
  304. return {
  305. 'originalSize': originalSize,
  306. 'compressedSize': gzipSize,
  307. 'compressionRatio': '$compressionRatio%',
  308. 'spaceSaved': originalSize - gzipSize,
  309. };
  310. } catch (e) {
  311. log(TAG, 'get compression info failed: $e');
  312. return {};
  313. }
  314. }
  315. }