tamar-file-hub-client 0.1.2__py3-none-any.whl → 0.1.4__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.
@@ -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
 
@@ -39,6 +39,7 @@ class AsyncBlobService(BaseFileService):
39
39
  mime_type: Optional[str] = None,
40
40
  is_temporary: Optional[bool] = False,
41
41
  expire_seconds: Optional[int] = None,
42
+ keep_original_filename: Optional[bool] = False,
42
43
  request_id: Optional[str] = None,
43
44
  **metadata
44
45
  ) -> FileUploadResponse:
@@ -53,6 +54,7 @@ class AsyncBlobService(BaseFileService):
53
54
  mime_type: MIME类型
54
55
  is_temporary: 是否为临时文件
55
56
  expire_seconds: 过期秒数
57
+ keep_original_filename: 是否保留原始文件名(默认False)
56
58
  request_id: 请求ID(可选,如果不提供则自动生成)
57
59
  **metadata: 额外的元数据(如 x-org-id, x-user-id 等)
58
60
 
@@ -86,6 +88,7 @@ class AsyncBlobService(BaseFileService):
86
88
  mime_type=mime_type or "application/octet-stream",
87
89
  is_temporary=is_temporary,
88
90
  expire_seconds=expire_seconds if is_temporary and expire_seconds else None,
91
+ keep_original_filename=keep_original_filename,
89
92
  )
90
93
 
91
94
  if folder_id:
@@ -114,10 +117,11 @@ class AsyncBlobService(BaseFileService):
114
117
  file_hash: str,
115
118
  is_temporary: Optional[bool] = False,
116
119
  expire_seconds: Optional[int] = None,
120
+ keep_original_filename: Optional[bool] = False,
117
121
  request_id: Optional[str] = None,
118
122
  **metadata
119
123
  ) -> FileUploadResponse:
120
- """流式上传"""
124
+ """流式上传(GCS 直传)"""
121
125
 
122
126
  # 获取上传URL,以及对应的文件和上传文件信息
123
127
  upload_url_resp = await self.generate_upload_url(
@@ -129,6 +133,7 @@ class AsyncBlobService(BaseFileService):
129
133
  file_hash=file_hash,
130
134
  is_temporary=is_temporary,
131
135
  expire_seconds=expire_seconds if is_temporary and expire_seconds else None,
136
+ keep_original_filename=keep_original_filename,
132
137
  request_id=request_id,
133
138
  **metadata
134
139
  )
@@ -140,11 +145,17 @@ class AsyncBlobService(BaseFileService):
140
145
  upload_file=upload_url_resp.upload_file
141
146
  )
142
147
 
148
+ # 构建HTTP头,包含Content-Type和固定的Cache-Control
149
+ headers = {
150
+ "Content-Type": mime_type,
151
+ "Cache-Control": "public, max-age=86400" # 24小时公共缓存
152
+ }
153
+
143
154
  # 上传文件到对象存储
144
155
  await self.http_uploader.upload(
145
156
  url=upload_url_resp.upload_url,
146
157
  content=content,
147
- headers={"Content-Type": mime_type},
158
+ headers=headers,
148
159
  total_size=file_size,
149
160
  )
150
161
 
@@ -172,10 +183,12 @@ class AsyncBlobService(BaseFileService):
172
183
  file_hash: str,
173
184
  is_temporary: Optional[bool] = False,
174
185
  expire_seconds: Optional[int] = None,
186
+ keep_original_filename: Optional[bool] = False,
175
187
  request_id: Optional[str] = None,
176
188
  **metadata
177
189
  ) -> FileUploadResponse:
178
- """断点续传实现"""
190
+ """断点续传实现(GCS 直传)"""
191
+
179
192
  # 获取断点续传URL,以及对应的文件和上传文件信息
180
193
  upload_url_resp = await self.generate_resumable_upload_url(
181
194
  file_name=file_name,
@@ -186,6 +199,7 @@ class AsyncBlobService(BaseFileService):
186
199
  file_hash=file_hash,
187
200
  is_temporary=is_temporary,
188
201
  expire_seconds=expire_seconds if is_temporary and expire_seconds else None,
202
+ keep_original_filename=keep_original_filename,
189
203
  request_id=request_id,
190
204
  **metadata
191
205
  )
@@ -197,6 +211,12 @@ class AsyncBlobService(BaseFileService):
197
211
  upload_file=upload_url_resp.upload_file
198
212
  )
199
213
 
214
+ # 构建HTTP头,包含Content-Type和固定的Cache-Control
215
+ headers = {
216
+ "Content-Type": mime_type,
217
+ "Cache-Control": "public, max-age=86400" # 24小时公共缓存
218
+ }
219
+
200
220
  # 开启断点续传
201
221
  upload_url = await self.http_uploader.start_resumable_session(
202
222
  url=upload_url_resp.upload_url,
@@ -208,7 +228,7 @@ class AsyncBlobService(BaseFileService):
208
228
  await self.http_uploader.upload(
209
229
  url=upload_url,
210
230
  content=content,
211
- headers={"Content-Type": mime_type},
231
+ headers=headers,
212
232
  total_size=file_size,
213
233
  )
214
234
 
@@ -256,6 +276,7 @@ class AsyncBlobService(BaseFileService):
256
276
  file_hash: Optional[str] = None,
257
277
  is_temporary: Optional[bool] = False,
258
278
  expire_seconds: Optional[int] = None,
279
+ keep_original_filename: Optional[bool] = False,
259
280
  request_id: Optional[str] = None,
260
281
  **metadata
261
282
  ) -> UploadUrlResponse:
@@ -271,6 +292,8 @@ class AsyncBlobService(BaseFileService):
271
292
  file_hash: 文件哈希
272
293
  is_temporary: 是否为临时文件
273
294
  expire_seconds: 过期秒数
295
+ keep_original_filename: 是否保留原始文件名(默认False)
296
+ request_id: 请求ID(可选,如果不提供则自动生成)
274
297
  **metadata: 额外的元数据(如 x-org-id, x-user-id 等)
275
298
 
276
299
  Returns:
@@ -288,6 +311,7 @@ class AsyncBlobService(BaseFileService):
288
311
  file_hash=file_hash,
289
312
  is_temporary=is_temporary,
290
313
  expire_seconds=expire_seconds if is_temporary and expire_seconds else None,
314
+ keep_original_filename=keep_original_filename,
291
315
  )
292
316
 
293
317
  if folder_id:
@@ -314,6 +338,7 @@ class AsyncBlobService(BaseFileService):
314
338
  file_hash: str = None,
315
339
  is_temporary: Optional[bool] = False,
316
340
  expire_seconds: Optional[int] = None,
341
+ keep_original_filename: Optional[bool] = False,
317
342
  request_id: Optional[str] = None,
318
343
  **metadata
319
344
  ) -> UploadUrlResponse:
@@ -329,6 +354,7 @@ class AsyncBlobService(BaseFileService):
329
354
  file_hash: 文件哈希
330
355
  is_temporary: 是否为临时文件
331
356
  expire_seconds: 过期秒数
357
+ keep_original_filename: 是否保留原始文件名(默认False)
332
358
  request_id: 请求ID(可选,如果不提供则自动生成)
333
359
  **metadata: 额外的元数据(如 x-org-id, x-user-id 等)
334
360
 
@@ -349,6 +375,7 @@ class AsyncBlobService(BaseFileService):
349
375
  file_hash=file_hash,
350
376
  is_temporary=is_temporary,
351
377
  expire_seconds=expire_seconds if is_temporary and expire_seconds else None,
378
+ keep_original_filename=keep_original_filename,
352
379
  )
353
380
 
354
381
  if folder_id:
@@ -373,6 +400,7 @@ class AsyncBlobService(BaseFileService):
373
400
  mode: Optional[UploadMode] = UploadMode.NORMAL,
374
401
  is_temporary: Optional[bool] = False,
375
402
  expire_seconds: Optional[int] = None,
403
+ keep_original_filename: Optional[bool] = False,
376
404
  url: Optional[str] = None,
377
405
  file_name: Optional[str] = None,
378
406
  request_id: Optional[str] = None,
@@ -387,6 +415,7 @@ class AsyncBlobService(BaseFileService):
387
415
  mode: 上传模式(NORMAL/DIRECT/RESUMABLE/STREAM)
388
416
  is_temporary: 是否为临时文件
389
417
  expire_seconds: 过期秒数
418
+ keep_original_filename: 是否保留原始文件名(默认False)
390
419
  url: 要下载并上传的URL(可选)
391
420
  file_name: 当使用url参数时指定的文件名(可选)
392
421
  request_id: 请求ID(可选,如果不提供则自动生成)
@@ -397,6 +426,8 @@ class AsyncBlobService(BaseFileService):
397
426
 
398
427
  Note:
399
428
  必须提供 file 或 url 参数之一
429
+
430
+ Cache-Control 头在 GCS 直传模式(STREAM/RESUMABLE)下自动设置为 "public, max-age=86400"
400
431
  """
401
432
  # 参数验证:必须提供 file 或 url 之一
402
433
  if file is None and not url:
@@ -452,7 +483,7 @@ class AsyncBlobService(BaseFileService):
452
483
 
453
484
  # 根据上传模式执行不同的上传逻辑
454
485
  if mode == UploadMode.NORMAL:
455
- # 普通上传(通过gRPC
486
+ # 普通上传(通过gRPC)- 不需要 Cache-Control
456
487
  return await self._upload_file(
457
488
  file_name=extracted_file_name,
458
489
  content=content,
@@ -461,12 +492,13 @@ class AsyncBlobService(BaseFileService):
461
492
  mime_type=mime_type,
462
493
  is_temporary=is_temporary,
463
494
  expire_seconds=expire_seconds,
495
+ keep_original_filename=keep_original_filename,
464
496
  request_id=request_id,
465
497
  **metadata
466
498
  )
467
499
 
468
500
  elif mode == UploadMode.STREAM:
469
- # 流式上传(目前使用直传实现)
501
+ # 流式上传(目前使用直传实现)- 需要 Cache-Control
470
502
  return await self._upload_stream(
471
503
  file_name=extracted_file_name,
472
504
  content=content,
@@ -477,12 +509,13 @@ class AsyncBlobService(BaseFileService):
477
509
  file_hash=file_hash,
478
510
  is_temporary=is_temporary,
479
511
  expire_seconds=expire_seconds,
512
+ keep_original_filename=keep_original_filename,
480
513
  request_id=request_id,
481
514
  **metadata
482
515
  )
483
516
 
484
517
  elif mode == UploadMode.RESUMABLE:
485
- # 断点续传
518
+ # 断点续传 - 需要 Cache-Control
486
519
  return await self._upload_resumable(
487
520
  file_name=extracted_file_name,
488
521
  content=content,
@@ -493,6 +526,7 @@ class AsyncBlobService(BaseFileService):
493
526
  file_hash=file_hash,
494
527
  is_temporary=is_temporary,
495
528
  expire_seconds=expire_seconds,
529
+ keep_original_filename=keep_original_filename,
496
530
  request_id=request_id,
497
531
  **metadata
498
532
  )
@@ -569,6 +603,30 @@ class AsyncBlobService(BaseFileService):
569
603
  chunk_size=chunk_size,
570
604
  )
571
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
+
572
630
  async def batch_generate_download_url(
573
631
  self,
574
632
  file_ids: List[str],
@@ -691,3 +749,215 @@ class AsyncBlobService(BaseFileService):
691
749
  ))
692
750
 
693
751
  return BatchGcsUrlResponse(gcs_urls=gcs_urls)
752
+
753
+ async def get_compression_status(
754
+ self,
755
+ file_id: str,
756
+ *,
757
+ request_id: Optional[str] = None,
758
+ **metadata
759
+ ) -> CompressionStatusResponse:
760
+ """
761
+ 获取文件压缩状态
762
+
763
+ Args:
764
+ file_id: 文件ID
765
+ request_id: 请求ID,用于追踪
766
+ **metadata: 额外的gRPC元数据
767
+
768
+ Returns:
769
+ CompressionStatusResponse: 压缩状态响应
770
+ """
771
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
772
+
773
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
774
+
775
+ request = file_service_pb2.CompressionStatusRequest(file_id=file_id)
776
+
777
+ # 构建元数据
778
+ grpc_metadata = self.client.build_metadata(request_id=request_id, **metadata)
779
+
780
+ response = await stub.GetCompressionStatus(request, metadata=grpc_metadata)
781
+
782
+ # 转换压缩变体
783
+ variants = []
784
+ for variant in response.variants:
785
+ variants.append(CompressedVariant(
786
+ variant_name=variant.variant_name,
787
+ variant_type=variant.variant_type,
788
+ media_type=variant.media_type,
789
+ width=variant.width,
790
+ height=variant.height,
791
+ file_size=variant.file_size,
792
+ format=variant.format,
793
+ quality=variant.quality if variant.quality else None,
794
+ duration=variant.duration if variant.duration else None,
795
+ bitrate=variant.bitrate if variant.bitrate else None,
796
+ fps=variant.fps if variant.fps else None,
797
+ compression_ratio=variant.compression_ratio,
798
+ stored_path=variant.stored_path
799
+ ))
800
+
801
+ return CompressionStatusResponse(
802
+ status=response.status,
803
+ error_message=response.error_message if response.error_message else None,
804
+ variants=variants
805
+ )
806
+
807
+ async def get_compressed_variants(
808
+ self,
809
+ file_id: str,
810
+ *,
811
+ variant_type: Optional[str] = None,
812
+ request_id: Optional[str] = None,
813
+ **metadata
814
+ ) -> GetVariantsResponse:
815
+ """
816
+ 获取文件的压缩变体
817
+
818
+ Args:
819
+ file_id: 文件ID
820
+ variant_type: 变体类型(image, video, thumbnail)
821
+ request_id: 请求ID,用于追踪
822
+ **metadata: 额外的gRPC元数据
823
+
824
+ Returns:
825
+ GetVariantsResponse: 压缩变体响应
826
+ """
827
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
828
+
829
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
830
+
831
+ request = file_service_pb2.GetVariantsRequest(file_id=file_id)
832
+ if variant_type:
833
+ request.variant_type = variant_type
834
+
835
+ # 构建元数据
836
+ grpc_metadata = self.client.build_metadata(request_id=request_id, **metadata)
837
+
838
+ response = await stub.GetCompressedVariants(request, metadata=grpc_metadata)
839
+
840
+ # 转换压缩变体
841
+ variants = []
842
+ for variant in response.variants:
843
+ variants.append(CompressedVariant(
844
+ variant_name=variant.variant_name,
845
+ variant_type=variant.variant_type,
846
+ media_type=variant.media_type,
847
+ width=variant.width,
848
+ height=variant.height,
849
+ file_size=variant.file_size,
850
+ format=variant.format,
851
+ quality=variant.quality if variant.quality else None,
852
+ duration=variant.duration if variant.duration else None,
853
+ bitrate=variant.bitrate if variant.bitrate else None,
854
+ fps=variant.fps if variant.fps else None,
855
+ compression_ratio=variant.compression_ratio,
856
+ stored_path=variant.stored_path
857
+ ))
858
+
859
+ return GetVariantsResponse(variants=variants)
860
+
861
+ async def trigger_recompression(
862
+ self,
863
+ file_id: str,
864
+ *,
865
+ force_reprocess: bool = False,
866
+ request_id: Optional[str] = None,
867
+ **metadata
868
+ ) -> RecompressionResponse:
869
+ """
870
+ 触发文件重新压缩
871
+
872
+ Args:
873
+ file_id: 文件ID
874
+ force_reprocess: 是否强制重新处理
875
+ request_id: 请求ID,用于追踪
876
+ **metadata: 额外的gRPC元数据
877
+
878
+ Returns:
879
+ RecompressionResponse: 重新压缩响应
880
+ """
881
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
882
+
883
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
884
+
885
+ request = file_service_pb2.RecompressionRequest(
886
+ file_id=file_id,
887
+ force_reprocess=force_reprocess
888
+ )
889
+
890
+ # 构建元数据
891
+ grpc_metadata = self.client.build_metadata(request_id=request_id, **metadata)
892
+
893
+ response = await stub.TriggerRecompression(request, metadata=grpc_metadata)
894
+
895
+ return RecompressionResponse(
896
+ task_id=response.task_id,
897
+ status=response.status
898
+ )
899
+
900
+ async def generate_variant_download_url(
901
+ self,
902
+ file_id: str,
903
+ variant_name: str,
904
+ *,
905
+ expire_seconds: int = 3600,
906
+ is_cdn: bool = False,
907
+ request_id: Optional[str] = None,
908
+ **metadata
909
+ ) -> VariantDownloadUrlResponse:
910
+ """
911
+ 生成变体下载URL
912
+
913
+ Args:
914
+ file_id: 文件ID
915
+ variant_name: 变体名称(large/medium/small/thumbnail)
916
+ expire_seconds: 过期时间(秒)
917
+ is_cdn: 是否使用CDN
918
+ request_id: 请求ID,用于追踪
919
+ **metadata: 额外的gRPC元数据
920
+
921
+ Returns:
922
+ VariantDownloadUrlResponse: 变体下载URL响应
923
+ """
924
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
925
+
926
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
927
+
928
+ request = file_service_pb2.VariantDownloadUrlRequest(
929
+ file_id=file_id,
930
+ variant_name=variant_name,
931
+ expire_seconds=expire_seconds,
932
+ is_cdn=is_cdn
933
+ )
934
+
935
+ # 构建元数据
936
+ grpc_metadata = self.client.build_metadata(request_id=request_id, **metadata)
937
+
938
+ response = await stub.GenerateVariantDownloadUrl(request, metadata=grpc_metadata)
939
+
940
+ # 转换变体信息
941
+ variant_info = None
942
+ if response.variant_info:
943
+ variant_info = CompressedVariant(
944
+ variant_name=response.variant_info.variant_name,
945
+ variant_type=response.variant_info.variant_type,
946
+ media_type=response.variant_info.media_type,
947
+ width=response.variant_info.width,
948
+ height=response.variant_info.height,
949
+ file_size=response.variant_info.file_size,
950
+ format=response.variant_info.format,
951
+ quality=response.variant_info.quality if response.variant_info.quality else None,
952
+ duration=response.variant_info.duration if response.variant_info.duration else None,
953
+ bitrate=response.variant_info.bitrate if response.variant_info.bitrate else None,
954
+ fps=response.variant_info.fps if response.variant_info.fps else None,
955
+ compression_ratio=response.variant_info.compression_ratio,
956
+ stored_path=response.variant_info.stored_path
957
+ )
958
+
959
+ return VariantDownloadUrlResponse(
960
+ url=response.url,
961
+ error=response.error if response.error else None,
962
+ variant_info=variant_info
963
+ )
@@ -12,6 +12,11 @@ from ...schemas import (
12
12
  File,
13
13
  FileListResponse,
14
14
  GetFileResponse,
15
+ CompressionStatusResponse,
16
+ GetVariantsResponse,
17
+ RecompressionResponse,
18
+ VariantDownloadUrlResponse,
19
+ CompressedVariant,
15
20
  )
16
21
  from ...errors import FileNotFoundError
17
22
 
@@ -275,3 +280,215 @@ class AsyncFileService(BaseFileService):
275
280
  files = [self._convert_file_info(f) for f in response.files]
276
281
 
277
282
  return FileListResponse(files=files)
283
+
284
+ async def get_compression_status(
285
+ self,
286
+ file_id: str,
287
+ *,
288
+ request_id: Optional[str] = None,
289
+ **metadata
290
+ ) -> CompressionStatusResponse:
291
+ """
292
+ 获取文件压缩状态
293
+
294
+ Args:
295
+ file_id: 文件ID
296
+ request_id: 请求ID,用于追踪
297
+ **metadata: 额外的gRPC元数据
298
+
299
+ Returns:
300
+ CompressionStatusResponse: 压缩状态响应
301
+ """
302
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
303
+
304
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
305
+
306
+ request = file_service_pb2.CompressionStatusRequest(file_id=file_id)
307
+
308
+ # 构建元数据
309
+ grpc_metadata = self.client.build_metadata(request_id=request_id, **metadata)
310
+
311
+ response = await stub.GetCompressionStatus(request, metadata=grpc_metadata)
312
+
313
+ # 转换压缩变体
314
+ variants = []
315
+ for variant in response.variants:
316
+ variants.append(CompressedVariant(
317
+ variant_name=variant.variant_name,
318
+ variant_type=variant.variant_type,
319
+ media_type=variant.media_type,
320
+ width=variant.width,
321
+ height=variant.height,
322
+ file_size=variant.file_size,
323
+ format=variant.format,
324
+ quality=variant.quality if variant.quality else None,
325
+ duration=variant.duration if variant.duration else None,
326
+ bitrate=variant.bitrate if variant.bitrate else None,
327
+ fps=variant.fps if variant.fps else None,
328
+ compression_ratio=variant.compression_ratio,
329
+ stored_path=variant.stored_path
330
+ ))
331
+
332
+ return CompressionStatusResponse(
333
+ status=response.status,
334
+ error_message=response.error_message if response.error_message else None,
335
+ variants=variants
336
+ )
337
+
338
+ async def get_compressed_variants(
339
+ self,
340
+ file_id: str,
341
+ *,
342
+ variant_type: Optional[str] = None,
343
+ request_id: Optional[str] = None,
344
+ **metadata
345
+ ) -> GetVariantsResponse:
346
+ """
347
+ 获取文件的压缩变体
348
+
349
+ Args:
350
+ file_id: 文件ID
351
+ variant_type: 变体类型(image, video, thumbnail)
352
+ request_id: 请求ID,用于追踪
353
+ **metadata: 额外的gRPC元数据
354
+
355
+ Returns:
356
+ GetVariantsResponse: 压缩变体响应
357
+ """
358
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
359
+
360
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
361
+
362
+ request = file_service_pb2.GetVariantsRequest(file_id=file_id)
363
+ if variant_type:
364
+ request.variant_type = variant_type
365
+
366
+ # 构建元数据
367
+ grpc_metadata = self.client.build_metadata(request_id=request_id, **metadata)
368
+
369
+ response = await stub.GetCompressedVariants(request, metadata=grpc_metadata)
370
+
371
+ # 转换压缩变体
372
+ variants = []
373
+ for variant in response.variants:
374
+ variants.append(CompressedVariant(
375
+ variant_name=variant.variant_name,
376
+ variant_type=variant.variant_type,
377
+ media_type=variant.media_type,
378
+ width=variant.width,
379
+ height=variant.height,
380
+ file_size=variant.file_size,
381
+ format=variant.format,
382
+ quality=variant.quality if variant.quality else None,
383
+ duration=variant.duration if variant.duration else None,
384
+ bitrate=variant.bitrate if variant.bitrate else None,
385
+ fps=variant.fps if variant.fps else None,
386
+ compression_ratio=variant.compression_ratio,
387
+ stored_path=variant.stored_path
388
+ ))
389
+
390
+ return GetVariantsResponse(variants=variants)
391
+
392
+ async def trigger_recompression(
393
+ self,
394
+ file_id: str,
395
+ *,
396
+ force_reprocess: bool = False,
397
+ request_id: Optional[str] = None,
398
+ **metadata
399
+ ) -> RecompressionResponse:
400
+ """
401
+ 触发文件重新压缩
402
+
403
+ Args:
404
+ file_id: 文件ID
405
+ force_reprocess: 是否强制重新处理
406
+ request_id: 请求ID,用于追踪
407
+ **metadata: 额外的gRPC元数据
408
+
409
+ Returns:
410
+ RecompressionResponse: 重新压缩响应
411
+ """
412
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
413
+
414
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
415
+
416
+ request = file_service_pb2.RecompressionRequest(
417
+ file_id=file_id,
418
+ force_reprocess=force_reprocess
419
+ )
420
+
421
+ # 构建元数据
422
+ grpc_metadata = self.client.build_metadata(request_id=request_id, **metadata)
423
+
424
+ response = await stub.TriggerRecompression(request, metadata=grpc_metadata)
425
+
426
+ return RecompressionResponse(
427
+ task_id=response.task_id,
428
+ status=response.status
429
+ )
430
+
431
+ async def generate_variant_download_url(
432
+ self,
433
+ file_id: str,
434
+ variant_name: str,
435
+ *,
436
+ expire_seconds: int = 3600,
437
+ is_cdn: bool = False,
438
+ request_id: Optional[str] = None,
439
+ **metadata
440
+ ) -> VariantDownloadUrlResponse:
441
+ """
442
+ 生成变体下载URL
443
+
444
+ Args:
445
+ file_id: 文件ID
446
+ variant_name: 变体名称(large/medium/small/thumbnail)
447
+ expire_seconds: 过期时间(秒)
448
+ is_cdn: 是否使用CDN
449
+ request_id: 请求ID,用于追踪
450
+ **metadata: 额外的gRPC元数据
451
+
452
+ Returns:
453
+ VariantDownloadUrlResponse: 变体下载URL响应
454
+ """
455
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
456
+
457
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
458
+
459
+ request = file_service_pb2.VariantDownloadUrlRequest(
460
+ file_id=file_id,
461
+ variant_name=variant_name,
462
+ expire_seconds=expire_seconds,
463
+ is_cdn=is_cdn
464
+ )
465
+
466
+ # 构建元数据
467
+ grpc_metadata = self.client.build_metadata(request_id=request_id, **metadata)
468
+
469
+ response = await stub.GenerateVariantDownloadUrl(request, metadata=grpc_metadata)
470
+
471
+ # 转换变体信息
472
+ variant_info = None
473
+ if response.variant_info:
474
+ variant_info = CompressedVariant(
475
+ variant_name=response.variant_info.variant_name,
476
+ variant_type=response.variant_info.variant_type,
477
+ media_type=response.variant_info.media_type,
478
+ width=response.variant_info.width,
479
+ height=response.variant_info.height,
480
+ file_size=response.variant_info.file_size,
481
+ format=response.variant_info.format,
482
+ quality=response.variant_info.quality if response.variant_info.quality else None,
483
+ duration=response.variant_info.duration if response.variant_info.duration else None,
484
+ bitrate=response.variant_info.bitrate if response.variant_info.bitrate else None,
485
+ fps=response.variant_info.fps if response.variant_info.fps else None,
486
+ compression_ratio=response.variant_info.compression_ratio,
487
+ stored_path=response.variant_info.stored_path
488
+ )
489
+
490
+ return VariantDownloadUrlResponse(
491
+ url=response.url,
492
+ error=response.error if response.error else None,
493
+ variant_info=variant_info
494
+ )