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