tamar-file-hub-client 0.0.1__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.
Files changed (44) hide show
  1. file_hub_client/__init__.py +88 -0
  2. file_hub_client/client.py +414 -0
  3. file_hub_client/enums/__init__.py +12 -0
  4. file_hub_client/enums/export_format.py +16 -0
  5. file_hub_client/enums/role.py +7 -0
  6. file_hub_client/enums/upload_mode.py +11 -0
  7. file_hub_client/errors/__init__.py +30 -0
  8. file_hub_client/errors/exceptions.py +93 -0
  9. file_hub_client/py.typed +1 -0
  10. file_hub_client/rpc/__init__.py +10 -0
  11. file_hub_client/rpc/async_client.py +312 -0
  12. file_hub_client/rpc/gen/__init__.py +1 -0
  13. file_hub_client/rpc/gen/file_service_pb2.py +74 -0
  14. file_hub_client/rpc/gen/file_service_pb2_grpc.py +533 -0
  15. file_hub_client/rpc/gen/folder_service_pb2.py +53 -0
  16. file_hub_client/rpc/gen/folder_service_pb2_grpc.py +269 -0
  17. file_hub_client/rpc/generate_grpc.py +76 -0
  18. file_hub_client/rpc/protos/file_service.proto +147 -0
  19. file_hub_client/rpc/protos/folder_service.proto +65 -0
  20. file_hub_client/rpc/sync_client.py +313 -0
  21. file_hub_client/schemas/__init__.py +43 -0
  22. file_hub_client/schemas/context.py +160 -0
  23. file_hub_client/schemas/file.py +89 -0
  24. file_hub_client/schemas/folder.py +29 -0
  25. file_hub_client/services/__init__.py +17 -0
  26. file_hub_client/services/file/__init__.py +14 -0
  27. file_hub_client/services/file/async_blob_service.py +482 -0
  28. file_hub_client/services/file/async_file_service.py +257 -0
  29. file_hub_client/services/file/base_file_service.py +103 -0
  30. file_hub_client/services/file/sync_blob_service.py +478 -0
  31. file_hub_client/services/file/sync_file_service.py +255 -0
  32. file_hub_client/services/folder/__init__.py +10 -0
  33. file_hub_client/services/folder/async_folder_service.py +206 -0
  34. file_hub_client/services/folder/sync_folder_service.py +205 -0
  35. file_hub_client/utils/__init__.py +48 -0
  36. file_hub_client/utils/converter.py +108 -0
  37. file_hub_client/utils/download_helper.py +355 -0
  38. file_hub_client/utils/file_utils.py +105 -0
  39. file_hub_client/utils/retry.py +69 -0
  40. file_hub_client/utils/upload_helper.py +527 -0
  41. tamar_file_hub_client-0.0.1.dist-info/METADATA +874 -0
  42. tamar_file_hub_client-0.0.1.dist-info/RECORD +44 -0
  43. tamar_file_hub_client-0.0.1.dist-info/WHEEL +5 -0
  44. tamar_file_hub_client-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,478 @@
1
+ """
2
+ 同步二进制大对象服务
3
+ """
4
+ from pathlib import Path
5
+ from typing import Optional, Union, BinaryIO, Iterator
6
+
7
+ from .base_file_service import BaseFileService
8
+ from ...enums import UploadMode
9
+ from ...errors import ValidationError
10
+ from ...rpc import SyncGrpcClient
11
+ from ...schemas import FileUploadResponse, UploadUrlResponse
12
+ from ...utils import HttpUploader, HttpDownloader, retry_with_backoff, get_file_mime_type
13
+
14
+
15
+ class SyncBlobService(BaseFileService):
16
+ """同步文件(二进制大对象)服务"""
17
+
18
+ def __init__(self, client: SyncGrpcClient):
19
+ """
20
+ 初始化文件(二进制大对象)服务
21
+
22
+ Args:
23
+ client: 同步gRPC客户端
24
+ """
25
+ self.client = client
26
+ self.http_uploader = HttpUploader()
27
+ self.http_downloader = HttpDownloader()
28
+
29
+ def _generate_resumable_upload_url(
30
+ self,
31
+ file_name: str,
32
+ file_size: int,
33
+ folder_id: Optional[str] = None,
34
+ file_type: str = "file",
35
+ mime_type: str = None,
36
+ file_hash: str = None,
37
+ is_temporary: Optional[bool] = False,
38
+ expire_seconds: Optional[int] = None,
39
+ **metadata
40
+ ) -> UploadUrlResponse:
41
+ """
42
+ 生成断点续传URL
43
+
44
+ Args:
45
+ file_name: 文件名
46
+ file_size: 文件大小
47
+ folder_id: 文件夹ID
48
+ file_type: 文件类型
49
+ mime_type: MIME类型
50
+ file_hash: 文件哈希
51
+ is_temporary: 是否为临时文件
52
+ expire_seconds: 过期秒数
53
+ **metadata: 额外的元数据(如 x-org-id, x-user-id 等)
54
+
55
+ Returns:
56
+ 上传URL响应
57
+ """
58
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
59
+
60
+ stub = self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
61
+
62
+ request = file_service_pb2.UploadUrlRequest(
63
+ file_name=file_name,
64
+ file_size=file_size,
65
+ file_type=file_type,
66
+ mime_type=mime_type or "application/octet-stream",
67
+ file_hash=file_hash,
68
+ is_temporary=is_temporary,
69
+ expire_seconds=expire_seconds if is_temporary and expire_seconds else None,
70
+ )
71
+
72
+ if folder_id:
73
+ request.folder_id = folder_id
74
+
75
+ # 构建元数据
76
+ grpc_metadata = self.client.build_metadata(**metadata)
77
+
78
+ response = stub.GenerateResumableUploadUrl(request, metadata=grpc_metadata)
79
+
80
+ return UploadUrlResponse(
81
+ file=self._convert_file_info(response.file),
82
+ upload_file=self._convert_upload_file_info(response.upload_file),
83
+ upload_url=response.url
84
+ )
85
+
86
+ def _confirm_upload_completed(self, file_id: str, **metadata) -> None:
87
+ """
88
+ 确认上传完成
89
+
90
+ Args:
91
+ file_id: 文件ID
92
+ **metadata: 额外的元数据(如 x-org-id, x-user-id 等)
93
+ """
94
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
95
+
96
+ stub = self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
97
+
98
+ request = file_service_pb2.UploadCompletedRequest(file_id=file_id)
99
+
100
+ # 构建元数据
101
+ grpc_metadata = self.client.build_metadata(**metadata)
102
+
103
+ stub.ConfirmUploadCompleted(request, metadata=grpc_metadata)
104
+
105
+ @retry_with_backoff(max_retries=3)
106
+ def _upload_file(
107
+ self,
108
+ file_name: str,
109
+ content: Union[bytes, BinaryIO, Path],
110
+ folder_id: Optional[str] = None,
111
+ file_type: str = "file",
112
+ mime_type: Optional[str] = None,
113
+ is_temporary: Optional[bool] = False,
114
+ expire_seconds: Optional[int] = None,
115
+ **metadata
116
+ ) -> FileUploadResponse:
117
+ """
118
+ 直接上传文件
119
+
120
+ Args:
121
+ file_name: 文件名
122
+ content: 文件内容(字节、文件对象或路径)
123
+ folder_id: 文件夹ID
124
+ file_type: 文件类型
125
+ mime_type: MIME类型
126
+ is_temporary: 是否为临时文件
127
+ expire_seconds: 过期秒数
128
+ **metadata: 额外的元数据(如 x-org-id, x-user-id 等)
129
+
130
+ Returns:
131
+ 文件信息
132
+ """
133
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
134
+
135
+ stub = self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
136
+
137
+ # 处理不同类型的内容
138
+ if isinstance(content, Path):
139
+ if not content.exists():
140
+ raise ValidationError(f"文件不存在: {content}")
141
+ with open(content, "rb") as f:
142
+ file_bytes = f.read()
143
+ if not mime_type:
144
+ mime_type = get_file_mime_type(content)
145
+ elif isinstance(content, bytes):
146
+ file_bytes = content
147
+ elif hasattr(content, 'read'):
148
+ file_bytes = content.read()
149
+ else:
150
+ raise ValidationError("不支持的内容类型")
151
+
152
+ # 构建请求
153
+ request = file_service_pb2.UploadFileRequest(
154
+ file_name=file_name,
155
+ content=file_bytes,
156
+ file_type=file_type,
157
+ mime_type=mime_type or "application/octet-stream",
158
+ is_temporary=is_temporary,
159
+ expire_seconds=expire_seconds,
160
+ )
161
+
162
+ if folder_id:
163
+ request.folder_id = folder_id
164
+
165
+ # 构建元数据
166
+ grpc_metadata = self.client.build_metadata(**metadata)
167
+
168
+ # 发送请求
169
+ response = stub.UploadFile(request, metadata=grpc_metadata)
170
+
171
+ # 转换响应
172
+ return FileUploadResponse(
173
+ file=self._convert_file_info(response.file),
174
+ upload_file=self._convert_upload_file_info(response.upload_file),
175
+ )
176
+
177
+ def _upload_stream(
178
+ self,
179
+ file_name: str,
180
+ content: Union[bytes, BinaryIO, Path],
181
+ file_size: int,
182
+ folder_id: Optional[str],
183
+ file_type: str,
184
+ mime_type: str,
185
+ file_hash: str,
186
+ is_temporary: Optional[bool] = False,
187
+ expire_seconds: Optional[int] = None,
188
+ **metadata
189
+ ) -> FileUploadResponse:
190
+ """客户端直传实现"""
191
+ # 获取上传URL,以及对应的文件和上传文件信息
192
+ upload_url_resp = self.generate_upload_url(
193
+ file_name=file_name,
194
+ file_size=file_size,
195
+ folder_id=folder_id,
196
+ file_type=file_type,
197
+ mime_type=mime_type,
198
+ file_hash=file_hash,
199
+ is_temporary=is_temporary,
200
+ expire_seconds=expire_seconds if is_temporary and expire_seconds else None,
201
+ **metadata
202
+ )
203
+
204
+ # 上传文件到对象存储
205
+ self.http_uploader.upload(
206
+ url=upload_url_resp.upload_url,
207
+ content=content,
208
+ headers={"Content-Type": mime_type},
209
+ total_size=file_size,
210
+ )
211
+
212
+ # 确认上传完成
213
+ self._confirm_upload_completed(
214
+ file_id=upload_url_resp.file.id,
215
+ **metadata
216
+ )
217
+
218
+ # 返回文件信息
219
+ return FileUploadResponse(
220
+ file=upload_url_resp.file,
221
+ upload_file=upload_url_resp.upload_file
222
+ )
223
+
224
+ def _upload_resumable(
225
+ self,
226
+ file_name: str,
227
+ content: Union[bytes, BinaryIO, Path],
228
+ file_size: int,
229
+ folder_id: Optional[str],
230
+ file_type: str,
231
+ mime_type: str,
232
+ file_hash: str,
233
+ is_temporary: Optional[bool] = False,
234
+ expire_seconds: Optional[int] = None,
235
+ **metadata
236
+ ) -> FileUploadResponse:
237
+ """断点续传实现"""
238
+ # 获取断点续传URL,以及对应的文件和上传文件信息
239
+ upload_url_resp = self._generate_resumable_upload_url(
240
+ file_name=file_name,
241
+ file_size=file_size,
242
+ folder_id=folder_id,
243
+ file_type=file_type,
244
+ mime_type=mime_type,
245
+ file_hash=file_hash,
246
+ is_temporary=is_temporary,
247
+ expire_seconds=expire_seconds if is_temporary and expire_seconds else None,
248
+ **metadata
249
+ )
250
+
251
+ # 开启断点续传
252
+ upload_url = self.http_uploader.start_resumable_session(
253
+ url=upload_url_resp.upload_url,
254
+ total_file_size=file_size,
255
+ mine_type=mime_type,
256
+ )
257
+
258
+ # 上传文件到对象存储
259
+ self.http_uploader.upload(
260
+ url=upload_url,
261
+ content=content,
262
+ headers={"Content-Type": mime_type},
263
+ total_size=file_size,
264
+ is_resume=True
265
+ )
266
+
267
+ # 确认上传完成
268
+ self._confirm_upload_completed(
269
+ file_id=upload_url_resp.file.id,
270
+ **metadata
271
+ )
272
+
273
+ # 返回文件信息
274
+ return FileUploadResponse(
275
+ file=upload_url_resp.file,
276
+ upload_file=upload_url_resp.upload_file
277
+ )
278
+
279
+ def generate_upload_url(
280
+ self,
281
+ file_name: str,
282
+ file_size: int,
283
+ folder_id: Optional[str] = None,
284
+ file_type: str = "file",
285
+ mime_type: str = None,
286
+ file_hash: str = None,
287
+ is_temporary: Optional[bool] = False,
288
+ expire_seconds: Optional[int] = None,
289
+ **metadata
290
+ ) -> UploadUrlResponse:
291
+ """
292
+ 生成上传URL(用于客户端直传)
293
+
294
+ Args:
295
+ file_name: 文件名
296
+ file_size: 文件大小
297
+ folder_id: 文件夹ID
298
+ file_type: 文件类型
299
+ mime_type: MIME类型
300
+ file_hash: 文件哈希
301
+ is_temporary: 是否为临时文件
302
+ expire_seconds: 过期秒数
303
+ **metadata: 额外的元数据(如 x-org-id, x-user-id 等)
304
+
305
+ Returns:
306
+ 上传URL响应
307
+ """
308
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
309
+
310
+ stub = self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
311
+
312
+ request = file_service_pb2.UploadUrlRequest(
313
+ file_name=file_name,
314
+ file_size=file_size,
315
+ file_type=file_type,
316
+ mime_type=mime_type or "application/octet-stream",
317
+ file_hash=file_hash,
318
+ is_temporary=is_temporary,
319
+ expire_seconds=expire_seconds if is_temporary and expire_seconds else None,
320
+ )
321
+
322
+ if folder_id:
323
+ request.folder_id = folder_id
324
+
325
+ # 构建元数据
326
+ grpc_metadata = self.client.build_metadata(**metadata)
327
+
328
+ response = stub.GenerateUploadUrl(request, metadata=grpc_metadata)
329
+
330
+ return UploadUrlResponse(
331
+ file=self._convert_file_info(response.file),
332
+ upload_file=self._convert_upload_file_info(response.upload_file),
333
+ upload_url=response.url
334
+ )
335
+
336
+ def upload(
337
+ self,
338
+ file: Union[str, Path, BinaryIO, bytes],
339
+ *,
340
+ folder_id: Optional[str] = None,
341
+ mode: Optional[UploadMode] = UploadMode.NORMAL,
342
+ is_temporary: Optional[bool] = False,
343
+ expire_seconds: Optional[int] = None,
344
+ **metadata
345
+ ) -> FileUploadResponse:
346
+ """
347
+ 统一的文件上传接口
348
+
349
+ Args:
350
+ file: 文件路径、Path对象、文件对象或字节数据
351
+ folder_id: 目标文件夹ID(可选)
352
+ mode: 上传模式(NORMAL/DIRECT/RESUMABLE/STREAM)
353
+ is_temporary: 是否为临时文件
354
+ expire_seconds: 过期秒数
355
+ **metadata: 额外的元数据
356
+
357
+ Returns:
358
+ 文件信息
359
+ """
360
+ # 解析文件参数,提取文件信息
361
+ file_name, content, file_size, mime_type, file_type, file_hash = self._extract_file_info(file)
362
+
363
+ # 根据文件大小自动选择上传模式
364
+ if mode == UploadMode.NORMAL:
365
+ ten_mb = 1024 * 1024 * 10
366
+ hundred_mb = 1024 * 1024 * 100
367
+ if file_size >= ten_mb and file_size < hundred_mb: # 10MB
368
+ mode = UploadMode.STREAM # 大文件自动使用流式上传模式
369
+ elif file_size > hundred_mb:
370
+ mode = UploadMode.RESUMABLE # 特大文件自动使用断点续传模式
371
+
372
+ # 根据上传模式执行不同的上传逻辑
373
+ if mode == UploadMode.NORMAL:
374
+ # 普通上传(通过gRPC)
375
+ return self._upload_file(
376
+ file_name=file_name,
377
+ content=content,
378
+ folder_id=folder_id,
379
+ file_type=file_type,
380
+ mime_type=mime_type,
381
+ is_temporary=is_temporary,
382
+ expire_seconds=expire_seconds if is_temporary and expire_seconds else None,
383
+ **metadata
384
+ )
385
+
386
+ elif mode == UploadMode.STREAM:
387
+ # 流式上传
388
+ return self._upload_stream(
389
+ file_name=file_name,
390
+ content=content,
391
+ file_size=file_size,
392
+ folder_id=folder_id,
393
+ file_type=file_type,
394
+ mime_type=mime_type,
395
+ file_hash=file_hash,
396
+ is_temporary=is_temporary,
397
+ expire_seconds=expire_seconds if is_temporary and expire_seconds else None,
398
+ **metadata
399
+ )
400
+
401
+ elif mode == UploadMode.RESUMABLE:
402
+ # 断点续传
403
+ return self._upload_resumable(
404
+ file_name=file_name,
405
+ content=content,
406
+ file_size=file_size,
407
+ folder_id=folder_id,
408
+ file_type=file_type,
409
+ mime_type=mime_type,
410
+ file_hash=file_hash,
411
+ is_temporary=is_temporary,
412
+ expire_seconds=expire_seconds if is_temporary and expire_seconds else None,
413
+ **metadata
414
+ )
415
+
416
+ else:
417
+ raise ValidationError(f"不支持的上传模式: {mode}")
418
+
419
+ def generate_download_url(
420
+ self,
421
+ file_id: str,
422
+ *,
423
+ expire_seconds: Optional[int] = None,
424
+ **metadata
425
+ ) -> str:
426
+ """
427
+ 生成下载URL
428
+
429
+ Args:
430
+ file_id: 文件ID
431
+ **metadata: 额外的元数据(如 x-org-id, x-user-id 等)
432
+
433
+ Returns:
434
+ 下载URL
435
+ """
436
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
437
+
438
+ stub = self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
439
+
440
+ request = file_service_pb2.DownloadUrlRequest(file_id=file_id,
441
+ expire_seconds=expire_seconds if expire_seconds else None)
442
+
443
+ # 构建元数据
444
+ grpc_metadata = self.client.build_metadata(**metadata)
445
+
446
+ download_url_resp = stub.GenerateDownloadUrl(request, metadata=grpc_metadata)
447
+
448
+ return download_url_resp.url
449
+
450
+ def download(
451
+ self,
452
+ file_id: str,
453
+ save_path: Optional[Union[str, Path]] = None,
454
+ chunk_size: Optional[int] = None,
455
+ **metadata
456
+ ) -> Union[bytes, Path, Iterator[bytes]]:
457
+ """
458
+ 统一的文件下载接口
459
+
460
+ Args:
461
+ file_id: 文件ID
462
+ save_path: 保存路径(如果为None,返回字节数据)
463
+ chunk_size: 分片大小
464
+ **metadata: 额外的元数据
465
+
466
+ Returns:
467
+ - NORMAL模式:下载的内容(字节)或保存的文件路径
468
+ - STREAM模式:返回迭代器,逐块返回数据
469
+ """
470
+
471
+ # 获取下载URL
472
+ download_url = self.generate_download_url(file_id, **metadata)
473
+
474
+ return self.http_downloader.download(
475
+ url=download_url,
476
+ save_path=save_path,
477
+ chunk_size=chunk_size,
478
+ )