From 27aba268e295e198265705bea90f48b2dd23ee25 Mon Sep 17 00:00:00 2001 From: ngfchl Date: Sat, 18 Jul 2026 11:39:51 +0800 Subject: [PATCH] feat: add concurrent fast transfer parsing --- lib/models/fast_transfer.dart | 60 ++++++++++++++++ lib/pages/workspace_tools_page.dart | 108 +++++++++++++--------------- 2 files changed, 109 insertions(+), 59 deletions(-) create mode 100644 lib/models/fast_transfer.dart diff --git a/lib/models/fast_transfer.dart b/lib/models/fast_transfer.dart new file mode 100644 index 0000000..0edfb62 --- /dev/null +++ b/lib/models/fast_transfer.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; + +class FastTransferEntry { + final String path; + final int size; + final String? md5; + final String? gcid; + + const FastTransferEntry({ + required this.path, + required this.size, + this.md5, + this.gcid, + }); + + String get name => path.split('/').last; + String get directoryPath => + path.contains('/') ? path.substring(0, path.lastIndexOf('/')) : ''; + Map toJson() => { + 'path': path, + 'size': size, + if (md5 != null) 'etag': md5, + if (gcid != null) 'gcid': gcid, + }; +} + +List parseFastTransferJSON(String text) { + final root = jsonDecode(text); + final raw = root is Map ? root['files'] : root; + if (raw is! List) throw const FormatException('顶层需要包含 files 数组'); + final entries = []; + for (var index = 0; index < raw.length; index++) { + if (raw[index] is! Map) throw FormatException('第 ${index + 1} 项格式无效'); + final value = Map.from(raw[index] as Map); + final path = (value['path'] ?? value['filePath'] ?? value['name'] ?? '') + .toString() + .replaceAll('\\', '/') + .replaceFirst(RegExp(r'^/+'), ''); + final size = value['size'] is int + ? value['size'] as int + : int.tryParse('${value['size'] ?? value['fileSize'] ?? ''}'); + final md5 = (value['etag'] ?? value['eTag'] ?? value['md5']) + ?.toString() + .toLowerCase(); + final gcid = (value['gcid'] ?? value['gcId'])?.toString().toUpperCase(); + if (path.isEmpty || + path.split('/').contains('..') || + size == null || + size < 0 || + ((md5 == null || !RegExp(r'^[a-f0-9]{32}$').hasMatch(md5)) && + (gcid == null || !RegExp(r'^[A-F0-9]{40}$').hasMatch(gcid)))) { + throw FormatException('第 ${index + 1} 项缺少有效 path、size 或 MD5/GCID'); + } + entries.add( + FastTransferEntry(path: path, size: size, md5: md5, gcid: gcid), + ); + } + if (entries.isEmpty) throw const FormatException('files 不能为空'); + return entries; +} diff --git a/lib/pages/workspace_tools_page.dart b/lib/pages/workspace_tools_page.dart index a227613..9bafab9 100644 --- a/lib/pages/workspace_tools_page.dart +++ b/lib/pages/workspace_tools_page.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; @@ -7,6 +5,7 @@ import 'package:shadcn_ui/shadcn_ui.dart'; import '../core/storage/storage_manager.dart'; import '../models/cloud_file.dart'; import '../models/batch_rename.dart'; +import '../models/fast_transfer.dart'; import '../models/media_library.dart'; import '../providers/auth_provider.dart'; import '../providers/file_provider.dart'; @@ -489,9 +488,11 @@ class _FastTransferToolState extends ConsumerState<_FastTransferTool> { } Future _submit() async { - final entries = _parseEntries(_json.text); - if (entries.isEmpty) { - setState(() => _result = '未找到包含 name、size、gcid 的秒传条目。'); + late final List entries; + try { + entries = parseFastTransferJSON(_json.text); + } catch (error) { + setState(() => _result = error.toString()); return; } setState(() { @@ -499,21 +500,55 @@ class _FastTransferToolState extends ConsumerState<_FastTransferTool> { _result = ''; }); var completed = 0; + var failed = 0; + var nextIndex = 0; final api = ref.read(authProvider.notifier).api; final parentID = ref.read(fileProvider).folderPath.lastOrNull?.id; + final concurrency = + (int.tryParse( + StorageManager.get( + StorageKeys.fastTransferConcurrency, + ) ?? + '3', + ) ?? + 3) + .clamp(1, 20); try { - for (final entry in entries) { - await api.flashTransferGCIDToken( - name: entry.name, - fileSize: entry.size, - parentID: parentID, - gcid: entry.gcid, - ); - completed += 1; + Future worker() async { + while (nextIndex < entries.length) { + final entry = entries[nextIndex++]; + try { + if (entry.md5 != null) { + await api.flashTransferToken( + name: entry.name, + fileSize: entry.size, + parentID: parentID, + md5: entry.md5!, + ); + } else { + await api.flashTransferGCIDToken( + name: entry.name, + fileSize: entry.size, + parentID: parentID, + gcid: entry.gcid!, + ); + } + completed += 1; + } catch (_) { + failed += 1; + } + if (mounted) { + setState( + () => _result = '已处理 ${completed + failed}/${entries.length} 项', + ); + } + } } + + await Future.wait(List.generate(concurrency, (_) => worker())); await ref.read(fileProvider.notifier).loadFiles(); if (mounted) { - setState(() => _result = '已提交 $completed / ${entries.length} 个秒传任务。'); + setState(() => _result = '秒传完成:成功 $completed,失败 $failed'); } } catch (error) { if (mounted) { @@ -524,43 +559,6 @@ class _FastTransferToolState extends ConsumerState<_FastTransferTool> { } } - List<_TransferEntry> _parseEntries(String text) { - try { - final value = jsonDecode(text); - final output = <_TransferEntry>[]; - void visit(dynamic node) { - if (node is Map) { - final name = node['name']?.toString() ?? node['fileName']?.toString(); - final gcid = node['gcid']?.toString() ?? node['gcId']?.toString(); - final rawSize = node['size'] ?? node['fileSize']; - final size = rawSize is int - ? rawSize - : int.tryParse(rawSize?.toString() ?? ''); - if (name != null && - name.isNotEmpty && - gcid != null && - gcid.isNotEmpty && - size != null && - size >= 0) { - output.add(_TransferEntry(name, gcid, size)); - } - for (final child in node.values) { - visit(child); - } - } else if (node is List) { - for (final child in node) { - visit(child); - } - } - } - - visit(value); - return output; - } catch (_) { - return const []; - } - } - @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; @@ -606,14 +604,6 @@ class _FastTransferToolState extends ConsumerState<_FastTransferTool> { } } -class _TransferEntry { - final String name; - final String gcid; - final int size; - - const _TransferEntry(this.name, this.gcid, this.size); -} - class _ToolSection extends StatelessWidget { final String title; final String description;