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