tamar-file-hub-client 0.1.3__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
 
@@ -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],
@@ -710,3 +749,215 @@ class AsyncBlobService(BaseFileService):
710
749
  ))
711
750
 
712
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
+ )