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,257 @@
1
+ """
2
+ 异步文件服务
3
+ """
4
+ import asyncio
5
+
6
+ import grpc
7
+ from typing import Optional, Dict, List, Any
8
+
9
+ from .base_file_service import BaseFileService
10
+ from ...rpc.async_client import AsyncGrpcClient
11
+ from ...schemas import (
12
+ File,
13
+ FileListResponse,
14
+ )
15
+ from ...errors import FileNotFoundError
16
+
17
+
18
+ class AsyncFileService(BaseFileService):
19
+ """异步文件服务"""
20
+
21
+ def __init__(self, client: AsyncGrpcClient):
22
+ """
23
+ 初始化文件服务
24
+
25
+ Args:
26
+ client: 异步gRPC客户端
27
+ """
28
+ self.client = client
29
+
30
+ async def generate_share_link(
31
+ self,
32
+ file_id: str,
33
+ *,
34
+ is_public: bool = True,
35
+ access_scope: str = "view",
36
+ expire_seconds: int = 86400,
37
+ max_access: Optional[int] = None,
38
+ share_password: Optional[str] = None,
39
+ **metadata
40
+ ) -> str:
41
+ """
42
+ 生成分享链接
43
+
44
+ Args:
45
+ file_id: 文件ID
46
+ is_public: 是否公开
47
+ access_scope: 访问范围
48
+ expire_seconds: 过期时间(秒)
49
+ max_access: 最大访问次数
50
+ share_password: 访问密码
51
+ **metadata: 额外的元数据(如 x-org-id, x-user-id 等)
52
+
53
+ Returns:
54
+ 分享ID
55
+ """
56
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
57
+
58
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
59
+
60
+ request = file_service_pb2.ShareLinkRequest(
61
+ file_id=file_id,
62
+ is_public=is_public,
63
+ access_scope=access_scope,
64
+ expire_seconds=expire_seconds
65
+ )
66
+
67
+ if max_access is not None:
68
+ request.max_access = max_access
69
+ if share_password:
70
+ request.share_password = share_password
71
+
72
+ # 构建元数据
73
+ grpc_metadata = self.client.build_metadata(**metadata)
74
+
75
+ response = await stub.GenerateShareLink(request, metadata=grpc_metadata)
76
+
77
+ return response.file_share_id
78
+
79
+ async def visit_file(
80
+ self,
81
+ file_share_id: str,
82
+ access_type: str = "view",
83
+ access_duration: int = 0,
84
+ metadata: Optional[Dict[str, Any]] = None,
85
+ **extra_metadata
86
+ ) -> None:
87
+ """
88
+ 访问文件(通过分享链接)
89
+
90
+ Args:
91
+ file_share_id: 分享ID
92
+ access_type: 访问类型
93
+ access_duration: 访问时长
94
+ metadata: 元数据
95
+ **extra_metadata: 额外的元数据(如 x-org-id, x-user-id 等)
96
+ """
97
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
98
+ from google.protobuf import struct_pb2
99
+
100
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
101
+
102
+ # 转换metadata为Struct
103
+ struct_metadata = struct_pb2.Struct()
104
+ if metadata:
105
+ for key, value in metadata.items():
106
+ struct_metadata[key] = value
107
+
108
+ request = file_service_pb2.FileVisitRequest(
109
+ file_share_id=file_share_id,
110
+ access_type=access_type,
111
+ access_duration=access_duration,
112
+ metadata=struct_metadata
113
+ )
114
+
115
+ # 构建元数据
116
+ grpc_metadata = self.client.build_metadata(**extra_metadata)
117
+
118
+ await stub.VisitFile(request, metadata=grpc_metadata)
119
+
120
+ async def get_file(self, file_id: str, **metadata) -> File:
121
+ """
122
+ 获取文件信息
123
+
124
+ Args:
125
+ file_id: 文件ID
126
+ **metadata: 额外的元数据(如 x-org-id, x-user-id 等)
127
+
128
+ Returns:
129
+ 文件信息
130
+ """
131
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
132
+
133
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
134
+
135
+ request = file_service_pb2.GetFileRequest(file_id=file_id)
136
+
137
+ # 构建元数据
138
+ grpc_metadata = self.client.build_metadata(**metadata)
139
+
140
+ try:
141
+ response = await stub.GetFile(request, metadata=grpc_metadata)
142
+ return self._convert_file_info(response)
143
+ except grpc.RpcError as e:
144
+ if e.code() == grpc.StatusCode.NOT_FOUND:
145
+ raise FileNotFoundError(file_id)
146
+ raise
147
+
148
+ async def rename_file(self, file_id: str, new_name: str, **metadata) -> File:
149
+ """
150
+ 重命名文件
151
+
152
+ Args:
153
+ file_id: 文件ID
154
+ new_name: 新文件名
155
+ **metadata: 额外的元数据(如 x-org-id, x-user-id 等)
156
+
157
+ Returns:
158
+ 更新后的文件信息
159
+ """
160
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
161
+
162
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
163
+
164
+ request = file_service_pb2.RenameFileRequest(
165
+ file_id=file_id,
166
+ new_name=new_name
167
+ )
168
+
169
+ # 构建元数据
170
+ grpc_metadata = self.client.build_metadata(**metadata)
171
+
172
+ try:
173
+ response = await stub.RenameFile(request, metadata=grpc_metadata)
174
+ return self._convert_file_info(response)
175
+ except grpc.RpcError as e:
176
+ if e.code() == grpc.StatusCode.NOT_FOUND:
177
+ raise FileNotFoundError(file_id)
178
+ raise
179
+
180
+ async def delete_file(self, file_id: str, **metadata) -> None:
181
+ """
182
+ 删除文件
183
+
184
+ Args:
185
+ file_id: 文件ID
186
+ **metadata: 额外的元数据(如 x-org-id, x-user-id 等)
187
+ """
188
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
189
+
190
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
191
+
192
+ request = file_service_pb2.DeleteFileRequest(file_id=file_id)
193
+
194
+ # 构建元数据
195
+ grpc_metadata = self.client.build_metadata(**metadata)
196
+
197
+ try:
198
+ await stub.DeleteFile(request, metadata=grpc_metadata)
199
+ except grpc.RpcError as e:
200
+ if e.code() == grpc.StatusCode.NOT_FOUND:
201
+ raise FileNotFoundError(file_id)
202
+ raise
203
+
204
+ async def list_files(
205
+ self,
206
+ folder_id: Optional[str] = None,
207
+ file_name: Optional[str] = None,
208
+ file_type: Optional[List[str]] = None,
209
+ created_by_role: Optional[str] = None,
210
+ created_by: Optional[str] = None,
211
+ page_size: int = 20,
212
+ page: int = 1,
213
+ **metadata
214
+ ) -> FileListResponse:
215
+ """
216
+ 列出文件
217
+
218
+ Args:
219
+ folder_id: 文件夹ID
220
+ file_name: 文件名过滤
221
+ file_type: 文件类型过滤
222
+ created_by_role: 创建者角色过滤
223
+ created_by: 创建者过滤
224
+ page_size: 每页大小
225
+ page: 页码
226
+ **metadata: 额外的元数据(如 x-org-id, x-user-id 等)
227
+
228
+ Returns:
229
+ 文件列表响应
230
+ """
231
+ from ...rpc.gen import file_service_pb2, file_service_pb2_grpc
232
+
233
+ stub = await self.client.get_stub(file_service_pb2_grpc.FileServiceStub)
234
+
235
+ request = file_service_pb2.ListFilesRequest(
236
+ folder_id=folder_id,
237
+ page_size=page_size,
238
+ page=page
239
+ )
240
+
241
+ if file_name:
242
+ request.file_name = file_name
243
+ if file_type:
244
+ request.file_type.extend(file_type)
245
+ if created_by_role:
246
+ request.created_by_role = created_by_role
247
+ if created_by:
248
+ request.created_by = created_by
249
+
250
+ # 构建元数据
251
+ grpc_metadata = self.client.build_metadata(**metadata)
252
+
253
+ response = await stub.ListFiles(request, metadata=grpc_metadata)
254
+
255
+ files = [self._convert_file_info(f) for f in response.files]
256
+
257
+ return FileListResponse(files=files)
@@ -0,0 +1,103 @@
1
+ import hashlib
2
+ from pathlib import Path
3
+ from typing import Optional, Union, BinaryIO, Tuple, Any
4
+
5
+ from ...schemas import File, UploadFile
6
+ from ...utils.file_utils import get_file_mime_type
7
+ from ...errors import ValidationError, FileNotFoundError
8
+
9
+
10
+ class BaseFileService:
11
+ """
12
+ 文件服务核心逻辑,提供与上传/下载无关的通用工具方法。
13
+ """
14
+
15
+ def _extract_file_info(
16
+ self,
17
+ file: Union[str, Path, BinaryIO, bytes]
18
+ ) -> Tuple[Optional[str], bytes, int, str, str, str]:
19
+ """
20
+ 提取文件信息并返回统一的 bytes 内容与 SHA256 哈希
21
+
22
+ Returns:
23
+ (文件名, 内容(bytes), 文件大小, MIME类型, 文件扩展名, 文件hash)
24
+ """
25
+
26
+ def get_file_type_and_mime(file_path: Path) -> Tuple[str, str]:
27
+ return (
28
+ file_path.suffix.lstrip('.').lower() if file_path.suffix else '',
29
+ get_file_mime_type(file_path)
30
+ )
31
+
32
+ def calculate_sha256_and_bytes(f: BinaryIO) -> Tuple[bytes, str]:
33
+ sha256 = hashlib.sha256()
34
+ content = bytearray()
35
+ while chunk := f.read(4 * 1024 * 1024):
36
+ content.extend(chunk)
37
+ sha256.update(chunk)
38
+ f.seek(0) # 复位以防止外部再用
39
+ return bytes(content), sha256.hexdigest()
40
+
41
+ # Case 1: 文件路径
42
+ if isinstance(file, (str, Path)):
43
+ file_path = Path(file)
44
+ if not file_path.exists():
45
+ raise FileNotFoundError(f"文件不存在: {file_path}")
46
+ file_name = file_path.name
47
+ file_type, mime_type = get_file_type_and_mime(file_path)
48
+ with file_path.open("rb") as f:
49
+ content, file_hash = calculate_sha256_and_bytes(f)
50
+ file_size = len(content)
51
+ return file_name, content, file_size, mime_type, file_type, file_hash
52
+
53
+ # Case 2: 原始字节流
54
+ elif isinstance(file, bytes):
55
+ sha256 = hashlib.sha256(file).hexdigest()
56
+ return None, file, len(file), "application/octet-stream", '', sha256
57
+
58
+ # Case 3: 可读文件对象
59
+ elif hasattr(file, 'read'):
60
+ file_name = getattr(file, 'name', None)
61
+ file_type = Path(file_name).suffix.lstrip('.').lower() if file_name else ''
62
+ mime_type = get_file_mime_type(Path(file_name)) if file_name else "application/octet-stream"
63
+
64
+ if hasattr(file, 'seek'):
65
+ file.seek(0)
66
+ content, file_hash = calculate_sha256_and_bytes(file)
67
+ file_size = len(content)
68
+ return Path(file_name).name if file_name else None, content, file_size, mime_type, file_type, file_hash
69
+
70
+ else:
71
+ raise ValidationError(f"不支持的文件类型: {type(file)}")
72
+
73
+ def _convert_file_info(self, proto_file: Any) -> File:
74
+ """转换Proto文件信息为模型"""
75
+ from ...utils.converter import timestamp_to_datetime
76
+
77
+ return File(
78
+ id=proto_file.id,
79
+ folder_id=proto_file.folder_id,
80
+ file_name=proto_file.file_name,
81
+ file_type=proto_file.file_type,
82
+ created_at=timestamp_to_datetime(proto_file.created_at),
83
+ updated_at=timestamp_to_datetime(proto_file.updated_at)
84
+ )
85
+
86
+ def _convert_upload_file_info(self, proto_upload_file: Any) -> UploadFile:
87
+ """转换Proto文件信息为模型"""
88
+ from ...utils.converter import timestamp_to_datetime
89
+
90
+ return UploadFile(
91
+ id=proto_upload_file.id,
92
+ folder_id=proto_upload_file.folder_id,
93
+ storage_type=proto_upload_file.storage_type,
94
+ stored_name=proto_upload_file.stored_name,
95
+ stored_path=proto_upload_file.stored_path,
96
+ file_id=proto_upload_file.file_id,
97
+ file_name=proto_upload_file.file_name,
98
+ file_size=proto_upload_file.file_size,
99
+ file_ext=proto_upload_file.file_ext,
100
+ mime_type=proto_upload_file.mime_type,
101
+ created_at=timestamp_to_datetime(proto_upload_file.created_at),
102
+ updated_at=timestamp_to_datetime(proto_upload_file.updated_at)
103
+ )