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.
- file_hub_client/__init__.py +88 -0
- file_hub_client/client.py +414 -0
- file_hub_client/enums/__init__.py +12 -0
- file_hub_client/enums/export_format.py +16 -0
- file_hub_client/enums/role.py +7 -0
- file_hub_client/enums/upload_mode.py +11 -0
- file_hub_client/errors/__init__.py +30 -0
- file_hub_client/errors/exceptions.py +93 -0
- file_hub_client/py.typed +1 -0
- file_hub_client/rpc/__init__.py +10 -0
- file_hub_client/rpc/async_client.py +312 -0
- file_hub_client/rpc/gen/__init__.py +1 -0
- file_hub_client/rpc/gen/file_service_pb2.py +74 -0
- file_hub_client/rpc/gen/file_service_pb2_grpc.py +533 -0
- file_hub_client/rpc/gen/folder_service_pb2.py +53 -0
- file_hub_client/rpc/gen/folder_service_pb2_grpc.py +269 -0
- file_hub_client/rpc/generate_grpc.py +76 -0
- file_hub_client/rpc/protos/file_service.proto +147 -0
- file_hub_client/rpc/protos/folder_service.proto +65 -0
- file_hub_client/rpc/sync_client.py +313 -0
- file_hub_client/schemas/__init__.py +43 -0
- file_hub_client/schemas/context.py +160 -0
- file_hub_client/schemas/file.py +89 -0
- file_hub_client/schemas/folder.py +29 -0
- file_hub_client/services/__init__.py +17 -0
- file_hub_client/services/file/__init__.py +14 -0
- file_hub_client/services/file/async_blob_service.py +482 -0
- file_hub_client/services/file/async_file_service.py +257 -0
- file_hub_client/services/file/base_file_service.py +103 -0
- file_hub_client/services/file/sync_blob_service.py +478 -0
- file_hub_client/services/file/sync_file_service.py +255 -0
- file_hub_client/services/folder/__init__.py +10 -0
- file_hub_client/services/folder/async_folder_service.py +206 -0
- file_hub_client/services/folder/sync_folder_service.py +205 -0
- file_hub_client/utils/__init__.py +48 -0
- file_hub_client/utils/converter.py +108 -0
- file_hub_client/utils/download_helper.py +355 -0
- file_hub_client/utils/file_utils.py +105 -0
- file_hub_client/utils/retry.py +69 -0
- file_hub_client/utils/upload_helper.py +527 -0
- tamar_file_hub_client-0.0.1.dist-info/METADATA +874 -0
- tamar_file_hub_client-0.0.1.dist-info/RECORD +44 -0
- tamar_file_hub_client-0.0.1.dist-info/WHEEL +5 -0
- 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
|
+
)
|