tamar-file-hub-client 0.1.5__tar.gz → 0.1.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/PKG-INFO +51 -1
  2. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/README.md +50 -0
  3. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/schemas/context.py +171 -171
  4. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/services/file/async_blob_service.py +44 -11
  5. tamar_file_hub_client-0.1.7/file_hub_client/services/file/base_file_service.py +317 -0
  6. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/services/file/sync_blob_service.py +44 -11
  7. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/utils/__init__.py +10 -0
  8. tamar_file_hub_client-0.1.7/file_hub_client/utils/mime_extension_mapper.py +158 -0
  9. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/utils/upload_helper.py +36 -22
  10. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/setup.py +1 -1
  11. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/tamar_file_hub_client.egg-info/PKG-INFO +51 -1
  12. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/tamar_file_hub_client.egg-info/SOURCES.txt +1 -0
  13. tamar_file_hub_client-0.1.5/file_hub_client/services/file/base_file_service.py +0 -116
  14. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/MANIFEST.in +0 -0
  15. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/__init__.py +0 -0
  16. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/client.py +0 -0
  17. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/enums/__init__.py +0 -0
  18. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/enums/export_format.py +0 -0
  19. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/enums/role.py +0 -0
  20. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/enums/upload_mode.py +0 -0
  21. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/errors/__init__.py +0 -0
  22. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/errors/exceptions.py +0 -0
  23. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/py.typed +0 -0
  24. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/rpc/__init__.py +0 -0
  25. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/rpc/async_client.py +0 -0
  26. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/rpc/gen/__init__.py +0 -0
  27. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/rpc/gen/file_service_pb2.py +0 -0
  28. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/rpc/gen/file_service_pb2_grpc.py +0 -0
  29. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/rpc/gen/folder_service_pb2.py +0 -0
  30. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/rpc/gen/folder_service_pb2_grpc.py +0 -0
  31. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/rpc/gen/taple_service_pb2.py +0 -0
  32. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/rpc/gen/taple_service_pb2_grpc.py +0 -0
  33. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/rpc/interceptors.py +0 -0
  34. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/rpc/protos/file_service.proto +0 -0
  35. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/rpc/protos/folder_service.proto +0 -0
  36. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/rpc/protos/taple_service.proto +0 -0
  37. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/rpc/sync_client.py +0 -0
  38. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/schemas/__init__.py +0 -0
  39. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/schemas/file.py +0 -0
  40. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/schemas/folder.py +0 -0
  41. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/schemas/taple.py +0 -0
  42. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/services/__init__.py +0 -0
  43. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/services/file/__init__.py +0 -0
  44. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/services/file/async_file_service.py +0 -0
  45. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/services/file/sync_file_service.py +0 -0
  46. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/services/folder/__init__.py +0 -0
  47. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/services/folder/async_folder_service.py +0 -0
  48. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/services/folder/sync_folder_service.py +0 -0
  49. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/services/taple/__init__.py +0 -0
  50. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/services/taple/async_taple_service.py +0 -0
  51. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/services/taple/base_taple_service.py +0 -0
  52. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/services/taple/idempotent_taple_mixin.py +0 -0
  53. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/services/taple/sync_taple_service.py +0 -0
  54. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/utils/converter.py +0 -0
  55. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/utils/download_helper.py +0 -0
  56. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/utils/file_utils.py +0 -0
  57. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/utils/idempotency.py +0 -0
  58. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/utils/ip_detector.py +0 -0
  59. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/utils/logging.py +0 -0
  60. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/utils/retry.py +0 -0
  61. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/file_hub_client/utils/smart_retry.py +0 -0
  62. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/setup.cfg +0 -0
  63. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/tamar_file_hub_client.egg-info/dependency_links.txt +0 -0
  64. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/tamar_file_hub_client.egg-info/requires.txt +0 -0
  65. {tamar_file_hub_client-0.1.5 → tamar_file_hub_client-0.1.7}/tamar_file_hub_client.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tamar-file-hub-client
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: A Python SDK for gRPC-based file management system
5
5
  Home-page: https://github.com/Tamar-Edge-AI/file-hub-client
6
6
  Author: Oscar Ou
@@ -55,6 +55,8 @@ Dynamic: summary
55
55
  - 📂 **文件夹管理**:支持文件夹的创建、重命名、移动、删除
56
56
  - 🔗 **文件分享**:支持生成分享链接,设置访问权限和密码
57
57
  - 🔄 **多种上传方式**:支持直传、断点续传、客户端直传到对象存储
58
+ - 🎯 **智能MIME类型检测**:支持26+种主流文件格式的魔术字节检测和扩展名推断
59
+ - 🤖 **AI生成文件支持**:完美支持AI模型输出的字节数据+MIME类型组合上传
58
60
  - 🛡️ **错误处理**:完善的异常体系和错误重试机制
59
61
  - 🔒 **TLS/SSL 支持**:支持安全的加密连接,保护数据传输
60
62
  - 🔁 **自动重试**:连接失败时自动重试,提高可靠性
@@ -615,6 +617,34 @@ async with AsyncTamarFileHubClient() as client:
615
617
  file_info = await client.blobs.upload(f)
616
618
  ```
617
619
 
620
+ #### AI生成文件上传(新功能)
621
+
622
+ ```python
623
+ from file_hub_client import AsyncTamarFileHubClient
624
+
625
+ async with AsyncTamarFileHubClient() as client:
626
+ # AI模型返回的字节数据(图片、音频、视频等)
627
+ # 场景:AI生图模型返回WebP格式图片
628
+ ai_image_data = b"\x52\x49\x46\x46...." # WebP格式的字节数据
629
+
630
+ # 方式1:显式指定MIME类型(推荐用于AI生成内容)
631
+ file_info = await client.blobs.upload(
632
+ file=ai_image_data,
633
+ mime_type="image/webp" # 明确指定MIME类型
634
+ )
635
+ print(f"AI生成图片上传成功: {file_info.file.file_name}") # upload_xxx.webp
636
+
637
+ # 方式2:自动检测MIME类型(支持26+种格式)
638
+ file_info = await client.blobs.upload(file=ai_image_data)
639
+ # 系统会自动检测magic bytes并推断为WebP格式
640
+
641
+ # 支持的AI生成内容格式:
642
+ # 🖼️ 图片: PNG, JPEG, WebP, GIF, BMP等
643
+ # 🎵 音频: MP3, WAV, FLAC, AAC, OGG等
644
+ # 🎥 视频: MP4, MOV, WebM, AVI等
645
+ # 📄 文档: PDF, TXT等
646
+ ```
647
+
618
648
  #### 大文件上传(流式上传和断点续传)
619
649
 
620
650
  ```python
@@ -2327,6 +2357,11 @@ export TEST_USER_ID=test-user-456
2327
2357
  5. **错误处理**:妥善处理各种异常
2328
2358
  6. **资源清理**:使用 with 语句确保资源释放
2329
2359
  7. **并发控制**:合理使用并发避免服务器过载
2360
+ 8. **AI生成文件处理**:
2361
+ - ✅ **推荐**: 上传AI生成的字节数据时显式提供 `mime_type` 参数
2362
+ - ✅ **备选**: 依赖自动检测(支持26+种格式的magic bytes检测)
2363
+ - ✅ **兼容**: 无需修改现有代码,保持100%向下兼容
2364
+ - ⚠️ **注意**: 断点续传现已完全支持MIME类型传递
2330
2365
 
2331
2366
  ## 许可证
2332
2367
 
@@ -2338,6 +2373,21 @@ MIT License
2338
2373
 
2339
2374
  ## 更新日志
2340
2375
 
2376
+ ### v0.0.7 (2025-09)
2377
+ - **重大修复**: 修复MIME类型检测和文件扩展名推断功能
2378
+ - **断点续传修复**: 解决断点续传中的HTTP头部和签名验证问题
2379
+ - **AI生成文件支持**: 完善对AI生成内容(图片、视频、音频)的MIME类型处理
2380
+ - **新功能**: 新增 `mime_type` 参数支持,允许用户显式指定文件MIME类型
2381
+ - **魔术字节检测**: 增强内容检测,支持26+种主流文件格式的自动识别
2382
+ - **向下兼容**: 保持100%向下兼容,现有代码无需修改
2383
+ - **核心修复**:
2384
+ - 修复 `upload_helper.py` 中系统性拼写错误(`mine_type` → `mime_type`)
2385
+ - 修复断点续传缺失 `Cache-Control` 头部导致的400错误
2386
+ - 修复AI生成文件默认使用 `.dat` 扩展名的问题
2387
+ - 增强MIME类型到文件扩展名的映射(50+种MIME类型支持)
2388
+ - **文件格式支持**: PNG, JPEG, WebP, MP4, MP3, WAV, GIF, BMP, PDF等主流格式
2389
+ - **使用场景**: 完美支持AI模型输出的字节数据+MIME类型组合
2390
+
2341
2391
  ### v0.0.6 (2025-08)
2342
2392
  - 新增媒体文件压缩服务功能
2343
2393
  - 支持获取文件压缩状态 (get_compression_status)
@@ -9,6 +9,8 @@
9
9
  - 📂 **文件夹管理**:支持文件夹的创建、重命名、移动、删除
10
10
  - 🔗 **文件分享**:支持生成分享链接,设置访问权限和密码
11
11
  - 🔄 **多种上传方式**:支持直传、断点续传、客户端直传到对象存储
12
+ - 🎯 **智能MIME类型检测**:支持26+种主流文件格式的魔术字节检测和扩展名推断
13
+ - 🤖 **AI生成文件支持**:完美支持AI模型输出的字节数据+MIME类型组合上传
12
14
  - 🛡️ **错误处理**:完善的异常体系和错误重试机制
13
15
  - 🔒 **TLS/SSL 支持**:支持安全的加密连接,保护数据传输
14
16
  - 🔁 **自动重试**:连接失败时自动重试,提高可靠性
@@ -569,6 +571,34 @@ async with AsyncTamarFileHubClient() as client:
569
571
  file_info = await client.blobs.upload(f)
570
572
  ```
571
573
 
574
+ #### AI生成文件上传(新功能)
575
+
576
+ ```python
577
+ from file_hub_client import AsyncTamarFileHubClient
578
+
579
+ async with AsyncTamarFileHubClient() as client:
580
+ # AI模型返回的字节数据(图片、音频、视频等)
581
+ # 场景:AI生图模型返回WebP格式图片
582
+ ai_image_data = b"\x52\x49\x46\x46...." # WebP格式的字节数据
583
+
584
+ # 方式1:显式指定MIME类型(推荐用于AI生成内容)
585
+ file_info = await client.blobs.upload(
586
+ file=ai_image_data,
587
+ mime_type="image/webp" # 明确指定MIME类型
588
+ )
589
+ print(f"AI生成图片上传成功: {file_info.file.file_name}") # upload_xxx.webp
590
+
591
+ # 方式2:自动检测MIME类型(支持26+种格式)
592
+ file_info = await client.blobs.upload(file=ai_image_data)
593
+ # 系统会自动检测magic bytes并推断为WebP格式
594
+
595
+ # 支持的AI生成内容格式:
596
+ # 🖼️ 图片: PNG, JPEG, WebP, GIF, BMP等
597
+ # 🎵 音频: MP3, WAV, FLAC, AAC, OGG等
598
+ # 🎥 视频: MP4, MOV, WebM, AVI等
599
+ # 📄 文档: PDF, TXT等
600
+ ```
601
+
572
602
  #### 大文件上传(流式上传和断点续传)
573
603
 
574
604
  ```python
@@ -2281,6 +2311,11 @@ export TEST_USER_ID=test-user-456
2281
2311
  5. **错误处理**:妥善处理各种异常
2282
2312
  6. **资源清理**:使用 with 语句确保资源释放
2283
2313
  7. **并发控制**:合理使用并发避免服务器过载
2314
+ 8. **AI生成文件处理**:
2315
+ - ✅ **推荐**: 上传AI生成的字节数据时显式提供 `mime_type` 参数
2316
+ - ✅ **备选**: 依赖自动检测(支持26+种格式的magic bytes检测)
2317
+ - ✅ **兼容**: 无需修改现有代码,保持100%向下兼容
2318
+ - ⚠️ **注意**: 断点续传现已完全支持MIME类型传递
2284
2319
 
2285
2320
  ## 许可证
2286
2321
 
@@ -2292,6 +2327,21 @@ MIT License
2292
2327
 
2293
2328
  ## 更新日志
2294
2329
 
2330
+ ### v0.0.7 (2025-09)
2331
+ - **重大修复**: 修复MIME类型检测和文件扩展名推断功能
2332
+ - **断点续传修复**: 解决断点续传中的HTTP头部和签名验证问题
2333
+ - **AI生成文件支持**: 完善对AI生成内容(图片、视频、音频)的MIME类型处理
2334
+ - **新功能**: 新增 `mime_type` 参数支持,允许用户显式指定文件MIME类型
2335
+ - **魔术字节检测**: 增强内容检测,支持26+种主流文件格式的自动识别
2336
+ - **向下兼容**: 保持100%向下兼容,现有代码无需修改
2337
+ - **核心修复**:
2338
+ - 修复 `upload_helper.py` 中系统性拼写错误(`mine_type` → `mime_type`)
2339
+ - 修复断点续传缺失 `Cache-Control` 头部导致的400错误
2340
+ - 修复AI生成文件默认使用 `.dat` 扩展名的问题
2341
+ - 增强MIME类型到文件扩展名的映射(50+种MIME类型支持)
2342
+ - **文件格式支持**: PNG, JPEG, WebP, MP4, MP3, WAV, GIF, BMP, PDF等主流格式
2343
+ - **使用场景**: 完美支持AI模型输出的字节数据+MIME类型组合
2344
+
2295
2345
  ### v0.0.6 (2025-08)
2296
2346
  - 新增媒体文件压缩服务功能
2297
2347
  - 支持获取文件压缩状态 (get_compression_status)
@@ -1,171 +1,171 @@
1
- """
2
- 用户上下文和请求上下文相关的Schema定义
3
- """
4
- from dataclasses import dataclass, field
5
- from typing import Optional, Dict, Any
6
- from datetime import datetime
7
-
8
- from file_hub_client.enums import Role
9
-
10
-
11
- @dataclass
12
- class UserContext:
13
- """
14
- 用户上下文信息
15
-
16
- 包含两大类信息:
17
- 1. ownership(所有权): org_id, user_id - 表示资源归属
18
- 2. operator(操作者): actor_id, role - 表示实际操作者(可能是用户、agent或系统)
19
- 3. request info(请求信息): user_ip - 表示请求来源IP(用于审计和安全)
20
- """
21
- # Ownership - 资源所有权信息
22
- org_id: str # 组织ID
23
- user_id: str # 用户ID
24
-
25
- # Operator - 操作者信息
26
- actor_id: Optional[str] = None # 实际操作者ID(如果为空,默认使用user_id)
27
- role: Role = Role.ACCOUNT # 操作者角色(ACCOUNT, AGENT, SYSTEM等)
28
-
29
- # Request info - 请求信息
30
- user_ip: Optional[str] = None # 用户IP地址(请求来源IP)
31
-
32
- def __post_init__(self):
33
- """初始化后处理,如果actor_id为空,默认使用user_id"""
34
- if self.actor_id is None:
35
- self.actor_id = self.user_id
36
-
37
- def to_metadata(self) -> Dict[str, str]:
38
- """转换为gRPC metadata格式"""
39
- metadata = {
40
- 'x-org-id': self.org_id,
41
- 'x-user-id': self.user_id,
42
- 'x-actor-id': self.actor_id,
43
- 'x-role': self.role,
44
- }
45
-
46
- # 只有当user_ip不为None时才添加x-user-ip
47
- if self.user_ip is not None:
48
- metadata['x-user-ip'] = self.user_ip
49
-
50
- return metadata
51
-
52
- @classmethod
53
- def from_metadata(cls, metadata: Dict[str, str]) -> Optional['UserContext']:
54
- """从metadata中解析用户上下文"""
55
- org_id = metadata.get('x-org-id')
56
- user_id = metadata.get('x-user-id')
57
-
58
- if not org_id or not user_id:
59
- return None
60
-
61
- return cls(
62
- org_id=org_id,
63
- user_id=user_id,
64
- actor_id=metadata.get('x-actor-id'),
65
- role=Role(metadata.get('x-role', Role.ACCOUNT)),
66
- user_ip=metadata.get('x-user-ip')
67
- )
68
-
69
-
70
- @dataclass
71
- class RequestContext:
72
- """
73
- 请求上下文信息
74
-
75
- 包含请求相关的元数据,如客户端信息、请求追踪等
76
- """
77
- request_id: Optional[str] = None # 请求ID,用于追踪
78
- client_ip: Optional[str] = None # 客户端IP地址
79
- client_version: Optional[str] = None # 客户端版本
80
- client_type: Optional[str] = None # 客户端类型(web, mobile, desktop, cli等)
81
- user_agent: Optional[str] = None # User-Agent信息
82
- timestamp: Optional[datetime] = field(default_factory=datetime.now) # 请求时间戳
83
- extra: Dict[str, Any] = field(default_factory=dict) # 其他扩展信息
84
-
85
- def to_metadata(self) -> Dict[str, str]:
86
- """转换为gRPC metadata格式"""
87
- metadata = {}
88
-
89
- if self.request_id:
90
- metadata['x-request-id'] = self.request_id
91
- if self.client_ip:
92
- metadata['x-client-ip'] = self.client_ip
93
- if self.client_version:
94
- metadata['x-client-version'] = self.client_version
95
- if self.client_type:
96
- metadata['x-client-type'] = self.client_type
97
- if self.user_agent:
98
- metadata['x-user-agent'] = self.user_agent
99
- if self.timestamp:
100
- metadata['x-timestamp'] = self.timestamp.isoformat()
101
-
102
- # 添加扩展信息
103
- for key, value in self.extra.items():
104
- metadata[f'x-{key}'] = str(value)
105
-
106
- return metadata
107
-
108
- @classmethod
109
- def from_metadata(cls, metadata: Dict[str, str]) -> 'RequestContext':
110
- """从metadata中解析请求上下文"""
111
- # 提取标准字段
112
- request_id = metadata.get('x-request-id')
113
- client_ip = metadata.get('x-client-ip')
114
- client_version = metadata.get('x-client-version')
115
- client_type = metadata.get('x-client-type')
116
- user_agent = metadata.get('x-user-agent')
117
-
118
- # 解析时间戳
119
- timestamp = None
120
- if 'x-timestamp' in metadata:
121
- try:
122
- timestamp = datetime.fromisoformat(metadata['x-timestamp'])
123
- except:
124
- pass
125
-
126
- # 提取扩展字段
127
- extra = {}
128
- for key, value in metadata.items():
129
- if key.startswith('x-') and key not in [
130
- 'x-request-id', 'x-client-ip', 'x-client-version',
131
- 'x-client-type', 'x-user-agent', 'x-timestamp',
132
- 'x-org-id', 'x-user-id', 'x-actor-id', 'x-role'
133
- ]:
134
- extra[key[2:]] = value # 去掉 'x-' 前缀
135
-
136
- return cls(
137
- request_id=request_id,
138
- client_ip=client_ip,
139
- client_version=client_version,
140
- client_type=client_type,
141
- user_agent=user_agent,
142
- timestamp=timestamp,
143
- extra=extra
144
- )
145
-
146
-
147
- @dataclass
148
- class FullContext:
149
- """完整的上下文信息,包含用户上下文和请求上下文"""
150
- user_context: Optional[UserContext] = None
151
- request_context: Optional[RequestContext] = None
152
-
153
- def to_metadata(self) -> Dict[str, str]:
154
- """转换为gRPC metadata格式"""
155
- metadata = {}
156
-
157
- if self.user_context:
158
- metadata.update(self.user_context.to_metadata())
159
-
160
- if self.request_context:
161
- metadata.update(self.request_context.to_metadata())
162
-
163
- return metadata
164
-
165
- @classmethod
166
- def from_metadata(cls, metadata: Dict[str, str]) -> 'FullContext':
167
- """从metadata中解析完整上下文"""
168
- return cls(
169
- user_context=UserContext.from_metadata(metadata),
170
- request_context=RequestContext.from_metadata(metadata)
171
- )
1
+ """
2
+ 用户上下文和请求上下文相关的Schema定义
3
+ """
4
+ from dataclasses import dataclass, field
5
+ from typing import Optional, Dict, Any
6
+ from datetime import datetime
7
+
8
+ from file_hub_client.enums import Role
9
+
10
+
11
+ @dataclass
12
+ class UserContext:
13
+ """
14
+ 用户上下文信息
15
+
16
+ 包含两大类信息:
17
+ 1. ownership(所有权): org_id, user_id - 表示资源归属
18
+ 2. operator(操作者): actor_id, role - 表示实际操作者(可能是用户、agent或系统)
19
+ 3. request info(请求信息): user_ip - 表示请求来源IP(用于审计和安全)
20
+ """
21
+ # Ownership - 资源所有权信息
22
+ org_id: str # 组织ID
23
+ user_id: str # 用户ID
24
+
25
+ # Operator - 操作者信息
26
+ actor_id: Optional[str] = None # 实际操作者ID(如果为空,默认使用user_id)
27
+ role: Role = Role.ACCOUNT # 操作者角色(ACCOUNT, AGENT, SYSTEM等)
28
+
29
+ # Request info - 请求信息
30
+ user_ip: Optional[str] = None # 用户IP地址(请求来源IP)
31
+
32
+ def __post_init__(self):
33
+ """初始化后处理,如果actor_id为空,默认使用user_id"""
34
+ if self.actor_id is None:
35
+ self.actor_id = self.user_id
36
+
37
+ def to_metadata(self) -> Dict[str, str]:
38
+ """转换为gRPC metadata格式"""
39
+ metadata = {
40
+ 'x-org-id': self.org_id,
41
+ 'x-user-id': self.user_id,
42
+ 'x-actor-id': self.actor_id,
43
+ 'x-role': self.role,
44
+ }
45
+
46
+ # 只有当user_ip不为None时才添加x-user-ip
47
+ if self.user_ip is not None:
48
+ metadata['x-user-ip'] = self.user_ip
49
+
50
+ return metadata
51
+
52
+ @classmethod
53
+ def from_metadata(cls, metadata: Dict[str, str]) -> Optional['UserContext']:
54
+ """从metadata中解析用户上下文"""
55
+ org_id = metadata.get('x-org-id')
56
+ user_id = metadata.get('x-user-id')
57
+
58
+ if not org_id or not user_id:
59
+ return None
60
+
61
+ return cls(
62
+ org_id=org_id,
63
+ user_id=user_id,
64
+ actor_id=metadata.get('x-actor-id'),
65
+ role=Role(metadata.get('x-role', Role.ACCOUNT)),
66
+ user_ip=metadata.get('x-user-ip')
67
+ )
68
+
69
+
70
+ @dataclass
71
+ class RequestContext:
72
+ """
73
+ 请求上下文信息
74
+
75
+ 包含请求相关的元数据,如客户端信息、请求追踪等
76
+ """
77
+ request_id: Optional[str] = None # 请求ID,用于追踪
78
+ client_ip: Optional[str] = None # 客户端IP地址
79
+ client_version: Optional[str] = None # 客户端版本
80
+ client_type: Optional[str] = None # 客户端类型(web, mobile, desktop, cli等)
81
+ user_agent: Optional[str] = None # User-Agent信息
82
+ timestamp: Optional[datetime] = field(default_factory=datetime.now) # 请求时间戳
83
+ extra: Dict[str, Any] = field(default_factory=dict) # 其他扩展信息
84
+
85
+ def to_metadata(self) -> Dict[str, str]:
86
+ """转换为gRPC metadata格式"""
87
+ metadata = {}
88
+
89
+ if self.request_id:
90
+ metadata['x-request-id'] = self.request_id
91
+ if self.client_ip:
92
+ metadata['x-client-ip'] = self.client_ip
93
+ if self.client_version:
94
+ metadata['x-client-version'] = self.client_version
95
+ if self.client_type:
96
+ metadata['x-client-type'] = self.client_type
97
+ if self.user_agent:
98
+ metadata['x-user-agent'] = self.user_agent
99
+ if self.timestamp:
100
+ metadata['x-timestamp'] = self.timestamp.isoformat()
101
+
102
+ # 添加扩展信息
103
+ for key, value in self.extra.items():
104
+ metadata[f'x-{key}'] = str(value)
105
+
106
+ return metadata
107
+
108
+ @classmethod
109
+ def from_metadata(cls, metadata: Dict[str, str]) -> 'RequestContext':
110
+ """从metadata中解析请求上下文"""
111
+ # 提取标准字段
112
+ request_id = metadata.get('x-request-id')
113
+ client_ip = metadata.get('x-client-ip')
114
+ client_version = metadata.get('x-client-version')
115
+ client_type = metadata.get('x-client-type')
116
+ user_agent = metadata.get('x-user-agent')
117
+
118
+ # 解析时间戳
119
+ timestamp = None
120
+ if 'x-timestamp' in metadata:
121
+ try:
122
+ timestamp = datetime.fromisoformat(metadata['x-timestamp'])
123
+ except:
124
+ pass
125
+
126
+ # 提取扩展字段
127
+ extra = {}
128
+ for key, value in metadata.items():
129
+ if key.startswith('x-') and key not in [
130
+ 'x-request-id', 'x-client-ip', 'x-client-version',
131
+ 'x-client-type', 'x-user-agent', 'x-timestamp',
132
+ 'x-org-id', 'x-user-id', 'x-actor-id', 'x-role'
133
+ ]:
134
+ extra[key[2:]] = value # 去掉 'x-' 前缀
135
+
136
+ return cls(
137
+ request_id=request_id,
138
+ client_ip=client_ip,
139
+ client_version=client_version,
140
+ client_type=client_type,
141
+ user_agent=user_agent,
142
+ timestamp=timestamp,
143
+ extra=extra
144
+ )
145
+
146
+
147
+ @dataclass
148
+ class FullContext:
149
+ """完整的上下文信息,包含用户上下文和请求上下文"""
150
+ user_context: Optional[UserContext] = None
151
+ request_context: Optional[RequestContext] = None
152
+
153
+ def to_metadata(self) -> Dict[str, str]:
154
+ """转换为gRPC metadata格式"""
155
+ metadata = {}
156
+
157
+ if self.user_context:
158
+ metadata.update(self.user_context.to_metadata())
159
+
160
+ if self.request_context:
161
+ metadata.update(self.request_context.to_metadata())
162
+
163
+ return metadata
164
+
165
+ @classmethod
166
+ def from_metadata(cls, metadata: Dict[str, str]) -> 'FullContext':
167
+ """从metadata中解析完整上下文"""
168
+ return cls(
169
+ user_context=UserContext.from_metadata(metadata),
170
+ request_context=RequestContext.from_metadata(metadata)
171
+ )
@@ -221,7 +221,7 @@ class AsyncBlobService(BaseFileService):
221
221
  upload_url = await self.http_uploader.start_resumable_session(
222
222
  url=upload_url_resp.upload_url,
223
223
  total_file_size=file_size,
224
- mine_type=mime_type,
224
+ mime_type=mime_type,
225
225
  )
226
226
 
227
227
  # 上传文件到对象存储
@@ -403,6 +403,7 @@ class AsyncBlobService(BaseFileService):
403
403
  keep_original_filename: Optional[bool] = False,
404
404
  url: Optional[str] = None,
405
405
  file_name: Optional[str] = None,
406
+ mime_type: Optional[str] = None,
406
407
  request_id: Optional[str] = None,
407
408
  **metadata
408
409
  ) -> FileUploadResponse:
@@ -418,6 +419,7 @@ class AsyncBlobService(BaseFileService):
418
419
  keep_original_filename: 是否保留原始文件名(默认False)
419
420
  url: 要下载并上传的URL(可选)
420
421
  file_name: 当使用url参数时指定的文件名(可选)
422
+ mime_type: MIME类型(可选,用于推断文件扩展名,特别适用于AI生成的字节数据)
421
423
  request_id: 请求ID(可选,如果不提供则自动生成)
422
424
  **metadata: 额外的元数据
423
425
 
@@ -427,6 +429,8 @@ class AsyncBlobService(BaseFileService):
427
429
  Note:
428
430
  必须提供 file 或 url 参数之一
429
431
 
432
+ 当传入bytes或BinaryIO且未提供file_name时,建议提供mime_type以确保正确的文件扩展名推断
433
+
430
434
  Cache-Control 头在 GCS 直传模式(STREAM/RESUMABLE)下自动设置为 "public, max-age=86400"
431
435
  """
432
436
  # 参数验证:必须提供 file 或 url 之一
@@ -449,27 +453,56 @@ class AsyncBlobService(BaseFileService):
449
453
  # 使用下载的内容作为file参数
450
454
  file = downloaded_content
451
455
 
452
- # 提取文件信息(bytes会返回默认的MIME类型,我们稍后会基于文件名重新计算)
453
- _, content, file_size, _, _, file_hash = self._extract_file_info(file)
456
+ # MIME类型优先级:用户指定 > 内容检测 > URL文件名推断
457
+ if mime_type:
458
+ # 用户明确提供的MIME类型优先级最高,无需进行内容检测
459
+ final_mime_type = mime_type
460
+ else:
461
+ # 用户未提供MIME类型,进行内容检测和文件名推断
462
+ content_detected_mime = self._detect_mime_from_content(downloaded_content)
463
+ url_filename_mime = get_file_mime_type(Path(file_name))
464
+
465
+ if content_detected_mime != "application/octet-stream":
466
+ # 内容检测到了具体的MIME类型,使用内容检测的
467
+ final_mime_type = content_detected_mime
468
+ else:
469
+ # 内容检测失败,使用从URL文件名推断的MIME类型
470
+ final_mime_type = url_filename_mime
471
+
472
+ # 提取文件信息,传入最终确定的MIME类型
473
+ _, content, file_size, extract_mime_type, extract_file_type, file_hash = self._extract_file_info(file, final_mime_type)
454
474
 
455
475
  # file_name已经在上面设置了(要么是用户指定的,要么是从URL提取的)
456
476
  extracted_file_name = file_name
477
+
478
+ # 使用最终确定的MIME类型
479
+ mime_type = final_mime_type
457
480
 
458
- # 基于文件名计算文件类型和MIME类型
459
- file_type = Path(extracted_file_name).suffix.lstrip('.').lower() if Path(
460
- extracted_file_name).suffix else 'dat'
461
- mime_type = get_file_mime_type(Path(extracted_file_name))
481
+ # 基于最终MIME类型计算文件扩展名
482
+ file_type = extract_file_type
462
483
  else:
463
484
  # 解析文件参数,提取文件信息
464
- extracted_file_name, content, file_size, extract_mime_type, extract_file_type, file_hash = self._extract_file_info(
465
- file)
485
+ # 如果用户指定了文件名,先从文件名推断MIME类型,然后传给_extract_file_info
466
486
  if file_name:
487
+ # 用户指定了文件名,优先使用用户提供的MIME类型,否则从文件名推断
488
+ if mime_type:
489
+ file_name_mime_type = mime_type
490
+ else:
491
+ file_name_mime_type = get_file_mime_type(Path(file_name))
492
+ extracted_file_name, content, file_size, extract_mime_type, extract_file_type, file_hash = self._extract_file_info(
493
+ file, file_name_mime_type)
494
+ # 使用用户指定的文件名
467
495
  extracted_file_name = file_name
468
- mime_type = get_file_mime_type(file_name)
496
+ mime_type = file_name_mime_type
469
497
  file_type = Path(extracted_file_name).suffix.lstrip('.').lower() if Path(
470
498
  extracted_file_name).suffix else 'dat'
471
499
  else:
472
- mime_type = extract_mime_type
500
+ # 没有指定文件名,传入用户提供的MIME类型(如果有)
501
+ extracted_file_name, content, file_size, extract_mime_type, extract_file_type, file_hash = self._extract_file_info(
502
+ file, mime_type)
503
+ # 如果用户指定了MIME类型,使用用户指定的,否则使用检测的
504
+ if not mime_type:
505
+ mime_type = extract_mime_type
473
506
  file_type = extract_file_type
474
507
 
475
508
  # 根据文件大小自动选择上传模式