From ae7206346e7be3fb6f4f745fba7b06934dba7b39 Mon Sep 17 00:00:00 2001 From: ngfchl Date: Sat, 18 Jul 2026 11:44:01 +0800 Subject: [PATCH] feat: map fast transfer target directories --- lib/pages/workspace_tools_page.dart | 157 ++++++++++++++++++++++++++-- 1 file changed, 146 insertions(+), 11 deletions(-) diff --git a/lib/pages/workspace_tools_page.dart b/lib/pages/workspace_tools_page.dart index 9bafab9..0cb5fbd 100644 --- a/lib/pages/workspace_tools_page.dart +++ b/lib/pages/workspace_tools_page.dart @@ -479,6 +479,8 @@ class _FastTransferTool extends ConsumerStatefulWidget { class _FastTransferToolState extends ConsumerState<_FastTransferTool> { final _json = TextEditingController(); bool _running = false; + bool _createDirectories = true; + bool _skipExisting = true; String _result = ''; @override @@ -504,6 +506,7 @@ class _FastTransferToolState extends ConsumerState<_FastTransferTool> { var nextIndex = 0; final api = ref.read(authProvider.notifier).api; final parentID = ref.read(fileProvider).folderPath.lastOrNull?.id; + final directoryCache = {}; final concurrency = (int.tryParse( StorageManager.get( @@ -518,18 +521,29 @@ class _FastTransferToolState extends ConsumerState<_FastTransferTool> { while (nextIndex < entries.length) { final entry = entries[nextIndex++]; try { + final targetID = await _resolveTargetDirectory( + entry, + parentID, + api, + directoryCache, + ); + if (_skipExisting && + await _hasExistingFile(api, targetID, entry.name)) { + completed += 1; + continue; + } if (entry.md5 != null) { await api.flashTransferToken( name: entry.name, fileSize: entry.size, - parentID: parentID, + parentID: targetID, md5: entry.md5!, ); } else { await api.flashTransferGCIDToken( name: entry.name, fileSize: entry.size, - parentID: parentID, + parentID: targetID, gcid: entry.gcid!, ); } @@ -559,6 +573,105 @@ class _FastTransferToolState extends ConsumerState<_FastTransferTool> { } } + Future _resolveTargetDirectory( + FastTransferEntry entry, + String? rootID, + dynamic api, + Map cache, + ) async { + var currentID = rootID; + for (final name + in entry.directoryPath.split('/').where((part) => part.isNotEmpty)) { + if (name == '..') throw const FormatException('目录不能包含 ..'); + final key = '${currentID ?? 'root'}/$name'; + if (cache.containsKey(key)) { + currentID = cache[key]; + continue; + } + final children = _extractFiles( + await api.fsFiles(parentID: currentID, pageSize: 1000), + ); + final existing = children.where((file) => file.name == name).firstOrNull; + if (existing != null) { + if (!existing.isDirectory) { + throw FormatException('$name 已被同名文件占用'); + } + currentID = existing.id; + } else { + if (!_createDirectories) { + throw FormatException('${entry.path} 包含目录,请开启自动创建目录'); + } + final parentBeforeCreate = currentID; + final response = await api.fsCreateDir( + name, + parentID: parentBeforeCreate, + ); + currentID = _findString(response, const [ + 'fileId', + 'file_id', + 'id', + 'resId', + ]); + if (currentID == null) { + final refreshed = _extractFiles( + await api.fsFiles(parentID: parentBeforeCreate, pageSize: 1000), + ); + currentID = refreshed + .where((file) => file.name == name && file.isDirectory) + .firstOrNull + ?.id; + } + if (currentID == null) throw FormatException('无法创建目录 $name'); + } + cache[key] = currentID; + } + return currentID; + } + + Future _hasExistingFile( + dynamic api, + String? parentID, + String name, + ) async => _extractFiles( + await api.fsFiles(parentID: parentID, pageSize: 1000), + ).any((file) => !file.isDirectory && file.name == name); + + List _extractFiles(Map json) { + final files = []; + void visit(dynamic value) { + if (value is Map) { + try { + files.add(CloudFile.fromJson(Map.from(value))); + } catch (_) {} + for (final child in value.values) { + visit(child); + } + } else if (value is List) { + for (final child in value) { + visit(child); + } + } + } + + visit(json); + final unique = {for (final file in files) file.id: file}; + return unique.values.toList(); + } + + String? _findString(Map value, List keys) { + for (final key in keys) { + final found = value[key]; + if (found != null && found.toString().isNotEmpty) return found.toString(); + } + for (final child in value.values) { + if (child is Map) { + final found = _findString(Map.from(child), keys); + if (found != null) return found; + } + } + return null; + } + @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; @@ -580,16 +693,38 @@ class _FastTransferToolState extends ConsumerState<_FastTransferTool> { ), child: Padding( padding: const EdgeInsets.only(top: 12), - child: SizedBox( - height: 260, - child: ShadInput( - controller: _json, - maxLines: null, - expands: true, - placeholder: const Text( - '[{"name":"example.mkv","size":123,"gcid":"..."}]', + child: Column( + children: [ + Row( + children: [ + ShadCheckbox( + value: _createDirectories, + label: const Text('自动创建目录'), + onChanged: (value) => + setState(() => _createDirectories = value), + ), + const SizedBox(width: 16), + ShadCheckbox( + value: _skipExisting, + label: const Text('跳过同名文件'), + onChanged: (value) => + setState(() => _skipExisting = value), + ), + ], ), - ), + const SizedBox(height: 10), + SizedBox( + height: 220, + child: ShadInput( + controller: _json, + maxLines: null, + expands: true, + placeholder: const Text( + '{"files":[{"path":"Movies/example.mkv","size":123,"gcid":"..."}]}', + ), + ), + ), + ], ), ), ),