tamar-file-hub-client 0.1.3__py3-none-any.whl → 0.1.5__py3-none-any.whl

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.
@@ -1,126 +1,171 @@
1
- """
2
- 文件相关数据模型
3
- """
4
- from datetime import datetime
5
- from typing import Optional, Dict, List, Any
6
- from pydantic import BaseModel, Field
7
-
8
-
9
- class File(BaseModel):
10
- """文件信息模型"""
11
- id: str = Field(..., description="文件ID")
12
- folder_id: str = Field(..., description="所属文件夹ID")
13
- file_name: str = Field(..., description="原始文件名")
14
- file_type: str = Field(..., description="文件类型")
15
- created_at: datetime = Field(..., description="创建时间")
16
- updated_at: datetime = Field(..., description="更新时间")
17
-
18
- class Config:
19
- json_encoders = {
20
- datetime: lambda v: v.isoformat()
21
- }
22
-
23
-
24
- class UploadFile(BaseModel):
25
- """上传文件信息模型"""
26
- id: str = Field(..., description="上传文件ID")
27
- folder_id: str = Field(..., description="所属文件夹ID")
28
- storage_type: str = Field(..., description="存储类型")
29
- stored_name: str = Field(..., description="存储文件名")
30
- stored_path: str = Field(..., description="存储路径")
31
- file_id: str = Field(..., description="所属文件ID")
32
- file_name: str = Field(..., description="原始文件名")
33
- file_size: int = Field(0, description="文件大小(字节)")
34
- file_ext: str = Field(..., description="文件后缀")
35
- mime_type: str = Field(..., description="MIME类型")
36
- created_at: datetime = Field(..., description="创建时间")
37
- updated_at: datetime = Field(..., description="更新时间")
38
-
39
- class Config:
40
- json_encoders = {
41
- datetime: lambda v: v.isoformat()
42
- }
43
-
44
-
45
- class FileUploadResponse(BaseModel):
46
- """文件上传返回"""
47
- file: File = Field(..., description="文件信息")
48
- upload_file: UploadFile = Field(..., description="上传文件信息")
49
-
50
-
51
- class UploadUrlResponse(BaseModel):
52
- """上传URL响应"""
53
- file: File = Field(..., description="文件信息")
54
- upload_file: UploadFile = Field(..., description="上传文件信息")
55
- upload_url: str = Field(..., description="上传URL")
56
-
57
-
58
- class ShareLinkRequest(BaseModel):
59
- """生成分享链接请求"""
60
- file_id: str = Field(..., description="文件ID")
61
- is_public: bool = Field(True, description="是否公开")
62
- access_scope: str = Field("view", description="访问范围")
63
- expire_seconds: int = Field(86400, description="过期时间(秒)")
64
- max_access: Optional[int] = Field(None, description="最大访问次数")
65
- password: Optional[str] = Field(None, description="访问密码")
66
-
67
-
68
- class FileVisitRequest(BaseModel):
69
- """文件访问请求"""
70
- file_share_id: str = Field(..., description="分享ID")
71
- access_type: str = Field(..., description="访问类型")
72
- access_duration: int = Field(..., description="访问时长")
73
- metadata: Dict[str, Any] = Field(default_factory=dict, description="元数据")
74
-
75
-
76
- class FileListRequest(BaseModel):
77
- """文件列表请求"""
78
- folder_id: str = Field(..., description="文件夹ID")
79
- file_name: Optional[str] = Field(None, description="文件名")
80
- file_type: Optional[List[str]] = Field(None, description="文件类型")
81
- created_by_role: Optional[str] = Field(None, description="创建者角色")
82
- created_by: Optional[str] = Field(None, description="创建者")
83
- page_size: int = Field(20, description="每页大小")
84
- page: int = Field(1, description="页码")
85
-
86
-
87
- class FileListResponse(BaseModel):
88
- """文件列表响应"""
89
- files: List[File] = Field(default_factory=list, description="文件列表")
90
-
91
-
92
- class GetFileResponse(BaseModel):
93
- """获取文件响应"""
94
- file: File = Field(..., description="文件信息")
95
- upload_file: Optional[UploadFile] = Field(None, description="上传文件信息")
96
-
97
-
98
- class DownloadUrlInfo(BaseModel):
99
- """下载URL信息"""
100
- file_id: str = Field(..., description="文件ID")
101
- url: str = Field(..., description="下载URL")
102
- error: Optional[str] = Field(None, description="错误信息")
103
-
104
-
105
- class BatchDownloadUrlResponse(BaseModel):
106
- """批量下载URL响应"""
107
- download_urls: List[DownloadUrlInfo] = Field(default_factory=list, description="下载URL列表")
108
-
109
-
110
- class GcsUrlInfo(BaseModel):
111
- """GCS URL信息"""
112
- file_id: str = Field(..., description="文件ID")
113
- gcs_url: str = Field(..., description="GCS URL")
114
- mime_type: str = Field(..., description="MIME类型")
115
- error: Optional[str] = Field(None, description="错误信息")
116
-
117
-
118
- class GetGcsUrlResponse(BaseModel):
119
- """获取GCS URL响应"""
120
- gcs_url: str = Field(..., description="GCS URL")
121
- mime_type: str = Field(..., description="MIME类型")
122
-
123
-
124
- class BatchGcsUrlResponse(BaseModel):
125
- """批量GCS URL响应"""
126
- gcs_urls: List[GcsUrlInfo] = Field(default_factory=list, description="GCS URL列表")
1
+ """
2
+ 文件相关数据模型
3
+ """
4
+ from datetime import datetime
5
+ from typing import Optional, Dict, List, Any
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class File(BaseModel):
10
+ """文件信息模型"""
11
+ id: str = Field(..., description="文件ID")
12
+ folder_id: str = Field(..., description="所属文件夹ID")
13
+ file_name: str = Field(..., description="原始文件名")
14
+ file_type: str = Field(..., description="文件类型")
15
+ created_at: datetime = Field(..., description="创建时间")
16
+ updated_at: datetime = Field(..., description="更新时间")
17
+
18
+ class Config:
19
+ json_encoders = {
20
+ datetime: lambda v: v.isoformat()
21
+ }
22
+
23
+
24
+ class UploadFile(BaseModel):
25
+ """上传文件信息模型"""
26
+ id: str = Field(..., description="上传文件ID")
27
+ folder_id: str = Field(..., description="所属文件夹ID")
28
+ storage_type: str = Field(..., description="存储类型")
29
+ stored_name: str = Field(..., description="存储文件名")
30
+ stored_path: str = Field(..., description="存储路径")
31
+ file_id: str = Field(..., description="所属文件ID")
32
+ file_name: str = Field(..., description="原始文件名")
33
+ file_size: int = Field(0, description="文件大小(字节)")
34
+ file_ext: str = Field(..., description="文件后缀")
35
+ mime_type: str = Field(..., description="MIME类型")
36
+ created_at: datetime = Field(..., description="创建时间")
37
+ updated_at: datetime = Field(..., description="更新时间")
38
+
39
+ class Config:
40
+ json_encoders = {
41
+ datetime: lambda v: v.isoformat()
42
+ }
43
+
44
+
45
+ class FileUploadResponse(BaseModel):
46
+ """文件上传返回"""
47
+ file: File = Field(..., description="文件信息")
48
+ upload_file: UploadFile = Field(..., description="上传文件信息")
49
+
50
+
51
+ class UploadUrlResponse(BaseModel):
52
+ """上传URL响应"""
53
+ file: File = Field(..., description="文件信息")
54
+ upload_file: UploadFile = Field(..., description="上传文件信息")
55
+ upload_url: str = Field(..., description="上传URL")
56
+
57
+
58
+ class ShareLinkRequest(BaseModel):
59
+ """生成分享链接请求"""
60
+ file_id: str = Field(..., description="文件ID")
61
+ is_public: bool = Field(True, description="是否公开")
62
+ access_scope: str = Field("view", description="访问范围")
63
+ expire_seconds: int = Field(86400, description="过期时间(秒)")
64
+ max_access: Optional[int] = Field(None, description="最大访问次数")
65
+ password: Optional[str] = Field(None, description="访问密码")
66
+
67
+
68
+ class FileVisitRequest(BaseModel):
69
+ """文件访问请求"""
70
+ file_share_id: str = Field(..., description="分享ID")
71
+ access_type: str = Field(..., description="访问类型")
72
+ access_duration: int = Field(..., description="访问时长")
73
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="元数据")
74
+
75
+
76
+ class FileListRequest(BaseModel):
77
+ """文件列表请求"""
78
+ folder_id: str = Field(..., description="文件夹ID")
79
+ file_name: Optional[str] = Field(None, description="文件名")
80
+ file_type: Optional[List[str]] = Field(None, description="文件类型")
81
+ created_by_role: Optional[str] = Field(None, description="创建者角色")
82
+ created_by: Optional[str] = Field(None, description="创建者")
83
+ page_size: int = Field(20, description="每页大小")
84
+ page: int = Field(1, description="页码")
85
+
86
+
87
+ class FileListResponse(BaseModel):
88
+ """文件列表响应"""
89
+ files: List[File] = Field(default_factory=list, description="文件列表")
90
+
91
+
92
+ class GetFileResponse(BaseModel):
93
+ """获取文件响应"""
94
+ file: File = Field(..., description="文件信息")
95
+ upload_file: Optional[UploadFile] = Field(None, description="上传文件信息")
96
+
97
+
98
+ class DownloadUrlInfo(BaseModel):
99
+ """下载URL信息"""
100
+ file_id: str = Field(..., description="文件ID")
101
+ url: str = Field(..., description="下载URL")
102
+ mime_type: str = Field(..., description="MIME类型")
103
+ error: Optional[str] = Field(None, description="错误信息")
104
+
105
+
106
+ class BatchDownloadUrlResponse(BaseModel):
107
+ """批量下载URL响应"""
108
+ download_urls: List[DownloadUrlInfo] = Field(default_factory=list, description="下载URL列表")
109
+
110
+
111
+ class GcsUrlInfo(BaseModel):
112
+ """GCS URL信息"""
113
+ file_id: str = Field(..., description="文件ID")
114
+ gcs_url: str = Field(..., description="GCS URL")
115
+ mime_type: str = Field(..., description="MIME类型")
116
+ error: Optional[str] = Field(None, description="错误信息")
117
+
118
+
119
+ class GetGcsUrlResponse(BaseModel):
120
+ """获取GCS URL响应"""
121
+ gcs_url: str = Field(..., description="GCS URL")
122
+ mime_type: str = Field(..., description="MIME类型")
123
+
124
+
125
+ class BatchGcsUrlResponse(BaseModel):
126
+ """批量GCS URL响应"""
127
+ gcs_urls: List[GcsUrlInfo] = Field(default_factory=list, description="GCS URL列表")
128
+
129
+
130
+ # ========= 压缩服务相关模型 =========
131
+
132
+ class CompressedVariant(BaseModel):
133
+ """压缩变体信息"""
134
+ variant_name: str = Field(..., description="变体名称")
135
+ variant_type: str = Field(..., description="变体类型")
136
+ media_type: str = Field(..., description="媒体类型")
137
+ width: int = Field(..., description="宽度")
138
+ height: int = Field(..., description="高度")
139
+ file_size: int = Field(..., description="文件大小")
140
+ format: str = Field(..., description="格式")
141
+ quality: Optional[int] = Field(None, description="质量")
142
+ duration: Optional[float] = Field(None, description="时长")
143
+ bitrate: Optional[int] = Field(None, description="比特率")
144
+ fps: Optional[int] = Field(None, description="帧率")
145
+ compression_ratio: float = Field(..., description="压缩比")
146
+ stored_path: str = Field(..., description="存储路径")
147
+
148
+
149
+ class CompressionStatusResponse(BaseModel):
150
+ """压缩状态响应"""
151
+ status: str = Field(..., description="状态: pending, processing, completed, failed")
152
+ error_message: Optional[str] = Field(None, description="错误信息")
153
+ variants: List[CompressedVariant] = Field(default_factory=list, description="压缩变体列表")
154
+
155
+
156
+ class GetVariantsResponse(BaseModel):
157
+ """获取变体响应"""
158
+ variants: List[CompressedVariant] = Field(default_factory=list, description="压缩变体列表")
159
+
160
+
161
+ class RecompressionResponse(BaseModel):
162
+ """重新压缩响应"""
163
+ task_id: str = Field(..., description="任务ID")
164
+ status: str = Field(..., description="状态")
165
+
166
+
167
+ class VariantDownloadUrlResponse(BaseModel):
168
+ """变体下载URL响应"""
169
+ url: str = Field(..., description="下载URL")
170
+ error: Optional[str] = Field(None, description="错误信息")
171
+ variant_info: Optional[CompressedVariant] = Field(None, description="变体详细信息")
@@ -11,7 +11,7 @@ from .base_file_service import BaseFileService
11
11
  from ...enums import UploadMode
12
12
  from ...errors import ValidationError
13
13
  from ...rpc import AsyncGrpcClient
14
- from ...schemas import FileUploadResponse, UploadUrlResponse, BatchDownloadUrlResponse, DownloadUrlInfo, GcsUrlInfo, GetGcsUrlResponse, BatchGcsUrlResponse
14
+ from ...schemas import FileUploadResponse, UploadUrlResponse, BatchDownloadUrlResponse, DownloadUrlInfo, GcsUrlInfo, GetGcsUrlResponse, BatchGcsUrlResponse, CompressionStatusResponse, GetVariantsResponse, RecompressionResponse, VariantDownloadUrlResponse, CompressedVariant
15
15
  from ...utils import AsyncHttpUploader, AsyncHttpDownloader, retry_with_backoff, get_file_mime_type
16
16
 
17
17
 
@@ -121,7 +121,7 @@ class AsyncBlobService(BaseFileService):
121
121
  request_id: Optional[str] = None,
122
122
  **metadata
123
123
  ) -> FileUploadResponse:
124
- """流式上传"""
124
+ """流式上传(GCS 直传)"""
125
125
 
126
126
  # 获取上传URL,以及对应的文件和上传文件信息
127
127
  upload_url_resp = await self.generate_upload_url(
@@ -145,11 +145,17 @@ class AsyncBlobService(BaseFileService):
145
145
  upload_file=upload_url_resp.upload_file
146
146
  )
147
147
 
148
+ # 构建HTTP头,包含Content-Type和固定的Cache-Control
149
+ headers = {
150
+ "Content-Type": mime_type,
151
+ "Cache-Control": "public, max-age=86400" # 24小时公共缓存
152
+ }
153
+
148
154
  # 上传文件到对象存储
149
155
  await self.http_uploader.upload(
150
156
  url=upload_url_resp.upload_url,
151
157
  content=content,
152
- headers={"Content-Type": mime_type},
158
+ headers=headers,
153
159
  total_size=file_size,
154
160
  )
155
161
 
@@ -181,7 +187,8 @@ class AsyncBlobService(BaseFileService):
181
187
  request_id: Optional[str] = None,
182
188
  **metadata
183
189
  ) -> FileUploadResponse:
184
- """断点续传实现"""
190
+ """断点续传实现(GCS 直传)"""
191
+
185
192
  # 获取断点续传URL,以及对应的文件和上传文件信息
186
193
  upload_url_resp = await self.generate_resumable_upload_url(
187
194
  file_name=file_name,
@@ -204,6 +211,12 @@ class AsyncBlobService(BaseFileService):
204
211
  upload_file=upload_url_resp.upload_file
205
212
  )
206
213
 
214
+ # 构建HTTP头,包含Content-Type和固定的Cache-Control
215
+ headers = {
216
+ "Content-Type": mime_type,
217
+ "Cache-Control": "public, max-age=86400" # 24小时公共缓存
218
+ }
219
+
207
220
  # 开启断点续传
208
221
  upload_url = await self.http_uploader.start_resumable_session(
209
222
  url=upload_url_resp.upload_url,
@@ -215,7 +228,7 @@ class AsyncBlobService(BaseFileService):
215
228
  await self.http_uploader.upload(
216
229
  url=upload_url,
217
230
  content=content,
218
- headers={"Content-Type": mime_type},
231
+ headers=headers,
219
232
  total_size=file_size,
220
233
  )
221
234
 
@@ -413,6 +426,8 @@ class AsyncBlobService(BaseFileService):
413
426
 
414
427
  Note:
415
428
  必须提供 file 或 url 参数之一
429
+
430
+ Cache-Control 头在 GCS 直传模式(STREAM/RESUMABLE)下自动设置为 "public, max-age=86400"
416
431
  """
417
432
  # 参数验证:必须提供 file 或 url 之一
418
433
  if file is None and not url:
@@ -468,7 +483,7 @@ class AsyncBlobService(BaseFileService):
468
483
 
469
484
  # 根据上传模式执行不同的上传逻辑
470
485
  if mode == UploadMode.NORMAL:
471
- # 普通上传(通过gRPC
486
+ # 普通上传(通过gRPC)- 不需要 Cache-Control
472
487
  return await self._upload_file(
473
488
  file_name=extracted_file_name,
474
489
  content=content,
@@ -483,7 +498,7 @@ class AsyncBlobService(BaseFileService):
483
498
  )
484
499
 
485
500
  elif mode == UploadMode.STREAM:
486
- # 流式上传(目前使用直传实现)
501
+ # 流式上传(目前使用直传实现)- 需要 Cache-Control
487
502
  return await self._upload_stream(
488
503
  file_name=extracted_file_name,
489
504
  content=content,
@@ -500,7 +515,7 @@ class AsyncBlobService(BaseFileService):
500
515
  )
501
516
 
502
517
  elif mode == UploadMode.RESUMABLE:
503
- # 断点续传
518
+ # 断点续传 - 需要 Cache-Control
504
519
  return await self._upload_resumable(
505
520
  file_name=extracted_file_name,
506
521
  content=content,
@@ -588,6 +603,30 @@ class AsyncBlobService(BaseFileService):
588
603
  chunk_size=chunk_size,
589
604
  )
590
605
 
606
+ async def download_to_bytes(
607
+ self,
608
+ file_id: str,
609
+ *,
610
+ request_id: Optional[str] = None,
611
+ **metadata
612
+ ) -> bytes:
613
+ """
614
+ 下载文件并返回字节数据
615
+
616
+ Args:
617
+ file_id: 文件ID
618
+ request_id: 请求ID(可选,如果不提供则自动生成)
619
+ **metadata: 额外的元数据
620
+
621
+ Returns:
622
+ 文件的字节数据
623
+ """
624
+ # 获取下载URL
625
+ download_url = await self.generate_download_url(file_id, request_id=request_id, **metadata)
626
+
627
+ # 下载到内存并返回字节数据
628
+ return await self.http_downloader.download(url=download_url, save_path=None)
629
+
591
630
  async def batch_generate_download_url(
592
631
  self,
593
632
  file_ids: List[str],
@@ -631,6 +670,7 @@ class AsyncBlobService(BaseFileService):
631
670
  download_urls.append(DownloadUrlInfo(
632
671
  file_id=url_info.file_id,
633
672
  url=url_info.url,
673
+ mime_type=url_info.mime_type,
634
674
  error=url_info.error if url_info.HasField('error') else None
635
675
  ))
636
676
 
@@ -710,3 +750,215 @@ class AsyncBlobService(BaseFileService):
710
750
  ))
711
751
 
712
752
  return BatchGcsUrlResponse(gcs_urls=gcs_urls)
753
+
754
+ async def get_compression_status(
755
+ self,
756
+ file_id: str,
757
+ *,
758
+ request_id: Optional[str] = None,
759
+ **metadata
760
+ ) -> CompressionStatusResponse:
761
+ """
762
+ 获取文件压缩状态
763
+
764
+ Args:
765
+ file_id: 文件ID
766
+ request_id: 请求ID,用于追踪
767
+ **metadata: 额外的gRPC元数据
768
+
769
+ Returns:
770
+ CompressionStatusResponse: 压缩状态响应
771
+ """
772
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
773
+
774
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
775
+
776
+ request = file_service_pb2.CompressionStatusRequest(file_id=file_id)
777
+
778
+ # 构建元数据
779
+ grpc_metadata = self.client.build_metadata(request_id=request_id, **metadata)
780
+
781
+ response = await stub.GetCompressionStatus(request, metadata=grpc_metadata)
782
+
783
+ # 转换压缩变体
784
+ variants = []
785
+ for variant in response.variants:
786
+ variants.append(CompressedVariant(
787
+ variant_name=variant.variant_name,
788
+ variant_type=variant.variant_type,
789
+ media_type=variant.media_type,
790
+ width=variant.width,
791
+ height=variant.height,
792
+ file_size=variant.file_size,
793
+ format=variant.format,
794
+ quality=variant.quality if variant.quality else None,
795
+ duration=variant.duration if variant.duration else None,
796
+ bitrate=variant.bitrate if variant.bitrate else None,
797
+ fps=variant.fps if variant.fps else None,
798
+ compression_ratio=variant.compression_ratio,
799
+ stored_path=variant.stored_path
800
+ ))
801
+
802
+ return CompressionStatusResponse(
803
+ status=response.status,
804
+ error_message=response.error_message if response.error_message else None,
805
+ variants=variants
806
+ )
807
+
808
+ async def get_compressed_variants(
809
+ self,
810
+ file_id: str,
811
+ *,
812
+ variant_type: Optional[str] = None,
813
+ request_id: Optional[str] = None,
814
+ **metadata
815
+ ) -> GetVariantsResponse:
816
+ """
817
+ 获取文件的压缩变体
818
+
819
+ Args:
820
+ file_id: 文件ID
821
+ variant_type: 变体类型(image, video, thumbnail)
822
+ request_id: 请求ID,用于追踪
823
+ **metadata: 额外的gRPC元数据
824
+
825
+ Returns:
826
+ GetVariantsResponse: 压缩变体响应
827
+ """
828
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
829
+
830
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
831
+
832
+ request = file_service_pb2.GetVariantsRequest(file_id=file_id)
833
+ if variant_type:
834
+ request.variant_type = variant_type
835
+
836
+ # 构建元数据
837
+ grpc_metadata = self.client.build_metadata(request_id=request_id, **metadata)
838
+
839
+ response = await stub.GetCompressedVariants(request, metadata=grpc_metadata)
840
+
841
+ # 转换压缩变体
842
+ variants = []
843
+ for variant in response.variants:
844
+ variants.append(CompressedVariant(
845
+ variant_name=variant.variant_name,
846
+ variant_type=variant.variant_type,
847
+ media_type=variant.media_type,
848
+ width=variant.width,
849
+ height=variant.height,
850
+ file_size=variant.file_size,
851
+ format=variant.format,
852
+ quality=variant.quality if variant.quality else None,
853
+ duration=variant.duration if variant.duration else None,
854
+ bitrate=variant.bitrate if variant.bitrate else None,
855
+ fps=variant.fps if variant.fps else None,
856
+ compression_ratio=variant.compression_ratio,
857
+ stored_path=variant.stored_path
858
+ ))
859
+
860
+ return GetVariantsResponse(variants=variants)
861
+
862
+ async def trigger_recompression(
863
+ self,
864
+ file_id: str,
865
+ *,
866
+ force_reprocess: bool = False,
867
+ request_id: Optional[str] = None,
868
+ **metadata
869
+ ) -> RecompressionResponse:
870
+ """
871
+ 触发文件重新压缩
872
+
873
+ Args:
874
+ file_id: 文件ID
875
+ force_reprocess: 是否强制重新处理
876
+ request_id: 请求ID,用于追踪
877
+ **metadata: 额外的gRPC元数据
878
+
879
+ Returns:
880
+ RecompressionResponse: 重新压缩响应
881
+ """
882
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
883
+
884
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
885
+
886
+ request = file_service_pb2.RecompressionRequest(
887
+ file_id=file_id,
888
+ force_reprocess=force_reprocess
889
+ )
890
+
891
+ # 构建元数据
892
+ grpc_metadata = self.client.build_metadata(request_id=request_id, **metadata)
893
+
894
+ response = await stub.TriggerRecompression(request, metadata=grpc_metadata)
895
+
896
+ return RecompressionResponse(
897
+ task_id=response.task_id,
898
+ status=response.status
899
+ )
900
+
901
+ async def generate_variant_download_url(
902
+ self,
903
+ file_id: str,
904
+ variant_name: str,
905
+ *,
906
+ expire_seconds: int = 3600,
907
+ is_cdn: bool = False,
908
+ request_id: Optional[str] = None,
909
+ **metadata
910
+ ) -> VariantDownloadUrlResponse:
911
+ """
912
+ 生成变体下载URL
913
+
914
+ Args:
915
+ file_id: 文件ID
916
+ variant_name: 变体名称(large/medium/small/thumbnail)
917
+ expire_seconds: 过期时间(秒)
918
+ is_cdn: 是否使用CDN
919
+ request_id: 请求ID,用于追踪
920
+ **metadata: 额外的gRPC元数据
921
+
922
+ Returns:
923
+ VariantDownloadUrlResponse: 变体下载URL响应
924
+ """
925
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
926
+
927
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
928
+
929
+ request = file_service_pb2.VariantDownloadUrlRequest(
930
+ file_id=file_id,
931
+ variant_name=variant_name,
932
+ expire_seconds=expire_seconds,
933
+ is_cdn=is_cdn
934
+ )
935
+
936
+ # 构建元数据
937
+ grpc_metadata = self.client.build_metadata(request_id=request_id, **metadata)
938
+
939
+ response = await stub.GenerateVariantDownloadUrl(request, metadata=grpc_metadata)
940
+
941
+ # 转换变体信息
942
+ variant_info = None
943
+ if response.variant_info:
944
+ variant_info = CompressedVariant(
945
+ variant_name=response.variant_info.variant_name,
946
+ variant_type=response.variant_info.variant_type,
947
+ media_type=response.variant_info.media_type,
948
+ width=response.variant_info.width,
949
+ height=response.variant_info.height,
950
+ file_size=response.variant_info.file_size,
951
+ format=response.variant_info.format,
952
+ quality=response.variant_info.quality if response.variant_info.quality else None,
953
+ duration=response.variant_info.duration if response.variant_info.duration else None,
954
+ bitrate=response.variant_info.bitrate if response.variant_info.bitrate else None,
955
+ fps=response.variant_info.fps if response.variant_info.fps else None,
956
+ compression_ratio=response.variant_info.compression_ratio,
957
+ stored_path=response.variant_info.stored_path
958
+ )
959
+
960
+ return VariantDownloadUrlResponse(
961
+ url=response.url,
962
+ error=response.error if response.error else None,
963
+ variant_info=variant_info
964
+ )