pixelarraythirdparty 1.0.7__py3-none-any.whl → 1.0.8__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.
- pixelarraythirdparty/__init__.py +2 -1
- pixelarraythirdparty/client.py +18 -29
- pixelarraythirdparty/cron/cron.py +37 -37
- pixelarraythirdparty/filestorage/__init__.py +6 -0
- pixelarraythirdparty/filestorage/filestorage.py +286 -0
- pixelarraythirdparty/order/order.py +37 -398
- pixelarraythirdparty/product/product.py +46 -47
- pixelarraythirdparty/user/user.py +44 -44
- {pixelarraythirdparty-1.0.7.dist-info → pixelarraythirdparty-1.0.8.dist-info}/METADATA +1 -1
- pixelarraythirdparty-1.0.8.dist-info/RECORD +17 -0
- pixelarraythirdparty-1.0.7.dist-info/RECORD +0 -15
- {pixelarraythirdparty-1.0.7.dist-info → pixelarraythirdparty-1.0.8.dist-info}/WHEEL +0 -0
- {pixelarraythirdparty-1.0.7.dist-info → pixelarraythirdparty-1.0.8.dist-info}/licenses/LICENSE +0 -0
- {pixelarraythirdparty-1.0.7.dist-info → pixelarraythirdparty-1.0.8.dist-info}/top_level.txt +0 -0
pixelarraythirdparty/__init__.py
CHANGED
|
@@ -10,7 +10,7 @@ PixelArray 第三方微服务客户端
|
|
|
10
10
|
- user: 用户管理模块
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
__version__ = "1.0.
|
|
13
|
+
__version__ = "1.0.8"
|
|
14
14
|
__author__ = "Lu qi"
|
|
15
15
|
__email__ = "qi.lu@pixelarrayai.com"
|
|
16
16
|
|
|
@@ -20,4 +20,5 @@ __all__ = [
|
|
|
20
20
|
"cron",
|
|
21
21
|
"user",
|
|
22
22
|
"order",
|
|
23
|
+
"filestorage",
|
|
23
24
|
]
|
pixelarraythirdparty/client.py
CHANGED
|
@@ -1,28 +1,5 @@
|
|
|
1
|
-
import requests
|
|
2
1
|
import aiohttp
|
|
3
|
-
from typing import Dict, Any,
|
|
4
|
-
import asyncio
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Client:
|
|
8
|
-
"""基础认证类,提供公共的 API key 认证和请求方法"""
|
|
9
|
-
|
|
10
|
-
def __init__(self, api_key: str):
|
|
11
|
-
self.base_url = "https://thirdparty.pixelarrayai.com"
|
|
12
|
-
self.api_key = api_key
|
|
13
|
-
self.headers = {
|
|
14
|
-
"Content-Type": "application/json",
|
|
15
|
-
"X-API-Key": self.api_key,
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
def _request(self, method: str, url: str, **kwargs) -> Tuple[Dict[str, Any], bool]:
|
|
19
|
-
"""统一的请求方法"""
|
|
20
|
-
resp = requests.request(
|
|
21
|
-
method, f"{self.base_url}{url}", headers=self.headers, **kwargs
|
|
22
|
-
)
|
|
23
|
-
if resp.status_code == 200 and resp.json().get("success") is True:
|
|
24
|
-
return resp.json().get("data", {}), True
|
|
25
|
-
return {}, False
|
|
2
|
+
from typing import Dict, Any, Tuple
|
|
26
3
|
|
|
27
4
|
|
|
28
5
|
class AsyncClient:
|
|
@@ -34,14 +11,26 @@ class AsyncClient:
|
|
|
34
11
|
"X-API-Key": self.api_key,
|
|
35
12
|
}
|
|
36
13
|
|
|
37
|
-
async def _request(
|
|
14
|
+
async def _request(
|
|
15
|
+
self, method: str, url: str, **kwargs
|
|
16
|
+
) -> Tuple[Dict[str, Any], bool]:
|
|
17
|
+
# 如果kwargs中有headers,则合并headers
|
|
18
|
+
headers = self.headers.copy()
|
|
19
|
+
if "headers" in kwargs:
|
|
20
|
+
headers.update(kwargs["headers"])
|
|
21
|
+
kwargs = {k: v for k, v in kwargs.items() if k != "headers"}
|
|
22
|
+
|
|
38
23
|
async with aiohttp.ClientSession() as session:
|
|
39
24
|
req_method = getattr(session, method.lower())
|
|
40
25
|
async with req_method(
|
|
41
|
-
f"{self.base_url}{url}", headers=
|
|
26
|
+
f"{self.base_url}{url}", headers=headers, **kwargs
|
|
42
27
|
) as resp:
|
|
43
28
|
if resp.status == 200:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
29
|
+
try:
|
|
30
|
+
result = await resp.json()
|
|
31
|
+
if result.get("success") is True:
|
|
32
|
+
return result.get("data", {}), True
|
|
33
|
+
except:
|
|
34
|
+
# 如果不是JSON响应,返回空
|
|
35
|
+
pass
|
|
47
36
|
return {}, False
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
from pixelarraythirdparty.client import
|
|
1
|
+
from pixelarraythirdparty.client import AsyncClient
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
class
|
|
5
|
-
def get_cron_status(self):
|
|
4
|
+
class CronManagerAsync(AsyncClient):
|
|
5
|
+
async def get_cron_status(self):
|
|
6
6
|
"""
|
|
7
|
-
获取Cron
|
|
8
|
-
|
|
7
|
+
获取Cron服务状态(异步版本)
|
|
8
|
+
|
|
9
9
|
功能说明:
|
|
10
10
|
获取Cron服务的运行状态,包括已注册的任务、工作节点状态等。
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
输入参数:
|
|
13
13
|
无
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
返回字段:
|
|
16
16
|
data (dict): Cron服务状态信息
|
|
17
17
|
- registered_tasks (dict): 已注册的任务列表,按工作节点分组
|
|
@@ -19,50 +19,50 @@ class CronManager(Client):
|
|
|
19
19
|
- scheduled_task_count (int): 定时任务数量
|
|
20
20
|
- timestamp (str): 状态获取时间
|
|
21
21
|
success (bool): 操作是否成功
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
异常情况:
|
|
24
24
|
- 获取Cron状态失败:返回错误信息"获取Cron状态失败"
|
|
25
25
|
"""
|
|
26
|
-
data, success = self._request("GET", "/api/cron/status")
|
|
26
|
+
data, success = await self._request("GET", "/api/cron/status")
|
|
27
27
|
if not success:
|
|
28
28
|
return {}, False
|
|
29
29
|
return data, True
|
|
30
30
|
|
|
31
|
-
def get_cron_tasks(self):
|
|
31
|
+
async def get_cron_tasks(self):
|
|
32
32
|
"""
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
获取已注册任务列表(异步版本)
|
|
34
|
+
|
|
35
35
|
功能说明:
|
|
36
36
|
获取所有已注册的Cron任务列表。
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
输入参数:
|
|
39
39
|
无
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
返回字段:
|
|
42
42
|
data (dict): 任务列表信息
|
|
43
43
|
- tasks (list): 已注册的任务名称列表
|
|
44
44
|
- count (int): 任务数量
|
|
45
45
|
- timestamp (str): 获取时间
|
|
46
46
|
success (bool): 操作是否成功
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
异常情况:
|
|
49
49
|
- 获取任务列表失败:返回错误信息"获取任务列表失败"
|
|
50
50
|
"""
|
|
51
|
-
data, success = self._request("GET", "/api/cron/tasks")
|
|
51
|
+
data, success = await self._request("GET", "/api/cron/tasks")
|
|
52
52
|
if not success:
|
|
53
53
|
return {}, False
|
|
54
54
|
return data, True
|
|
55
55
|
|
|
56
|
-
def get_cron_tasks_scheduled(self):
|
|
56
|
+
async def get_cron_tasks_scheduled(self):
|
|
57
57
|
"""
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
获取定时任务列表(异步版本)
|
|
59
|
+
|
|
60
60
|
功能说明:
|
|
61
61
|
获取所有配置的定时任务列表,包括任务详情、执行时间、状态等。
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
输入参数:
|
|
64
64
|
无
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
返回字段:
|
|
67
67
|
data (dict): 定时任务列表信息
|
|
68
68
|
- tasks (list): 定时任务列表
|
|
@@ -81,25 +81,25 @@ class CronManager(Client):
|
|
|
81
81
|
- count (int): 任务数量
|
|
82
82
|
- timestamp (str): 获取时间
|
|
83
83
|
success (bool): 操作是否成功
|
|
84
|
-
|
|
84
|
+
|
|
85
85
|
异常情况:
|
|
86
86
|
- 获取定时任务列表失败:返回错误信息"获取定时任务列表失败"
|
|
87
87
|
"""
|
|
88
|
-
data, success = self._request("GET", "/api/cron/tasks/scheduled")
|
|
88
|
+
data, success = await self._request("GET", "/api/cron/tasks/scheduled")
|
|
89
89
|
if not success:
|
|
90
90
|
return {}, False
|
|
91
91
|
return data, True
|
|
92
92
|
|
|
93
|
-
def get_cron_tasks_detail(self, task_name: str):
|
|
93
|
+
async def get_cron_tasks_detail(self, task_name: str):
|
|
94
94
|
"""
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
获取任务详情(异步版本)
|
|
96
|
+
|
|
97
97
|
功能说明:
|
|
98
98
|
根据任务名称获取指定任务的详细信息。
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
输入参数:
|
|
101
101
|
task_name (str): 任务名称,必填,需要URL编码
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
返回字段:
|
|
104
104
|
data (dict): 任务详细信息
|
|
105
105
|
- task_name (str): 任务名称
|
|
@@ -112,28 +112,28 @@ class CronManager(Client):
|
|
|
112
112
|
- registration_info (dict): 注册信息
|
|
113
113
|
- timestamp (str): 获取时间
|
|
114
114
|
success (bool): 操作是否成功
|
|
115
|
-
|
|
115
|
+
|
|
116
116
|
异常情况:
|
|
117
117
|
- 任务不存在:返回错误信息"任务不存在"
|
|
118
118
|
- 获取任务详情失败:返回错误信息"获取任务详情失败"
|
|
119
119
|
"""
|
|
120
|
-
data, success = self._request("GET", f"/api/cron/tasks/{task_name}")
|
|
120
|
+
data, success = await self._request("GET", f"/api/cron/tasks/{task_name}")
|
|
121
121
|
if not success:
|
|
122
122
|
return {}, False
|
|
123
123
|
return data, True
|
|
124
124
|
|
|
125
|
-
def trigger_cron_task(self, task_name: str, args: list, kwargs: dict):
|
|
125
|
+
async def trigger_cron_task(self, task_name: str, args: list, kwargs: dict):
|
|
126
126
|
"""
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
触发任务执行(异步版本)
|
|
128
|
+
|
|
129
129
|
功能说明:
|
|
130
130
|
手动触发指定任务的执行,支持传递参数。
|
|
131
|
-
|
|
131
|
+
|
|
132
132
|
输入参数:
|
|
133
133
|
task_name (str): 任务名称,必填,需要URL编码
|
|
134
134
|
args (list): 任务参数列表,可选
|
|
135
135
|
kwargs (dict): 任务关键字参数,可选
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
返回字段:
|
|
138
138
|
data (dict): 任务触发信息
|
|
139
139
|
- task_id (str): 任务ID
|
|
@@ -141,12 +141,12 @@ class CronManager(Client):
|
|
|
141
141
|
- status (str): 任务状态,初始为"PENDING"
|
|
142
142
|
- message (str): 触发消息
|
|
143
143
|
success (bool): 操作是否成功
|
|
144
|
-
|
|
144
|
+
|
|
145
145
|
异常情况:
|
|
146
146
|
- 任务不存在:返回错误信息"任务不存在"
|
|
147
147
|
- 任务触发失败:返回错误信息"任务触发失败"
|
|
148
148
|
"""
|
|
149
|
-
data, success = self._request(
|
|
149
|
+
data, success = await self._request(
|
|
150
150
|
"POST",
|
|
151
151
|
f"/api/cron/tasks/{task_name}/trigger",
|
|
152
152
|
json={"args": args, "kwargs": kwargs},
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
from pixelarraythirdparty.client import AsyncClient
|
|
2
|
+
from typing import Dict, Any, Optional, List, Tuple, Callable, Union
|
|
3
|
+
import os
|
|
4
|
+
import asyncio
|
|
5
|
+
import aiohttp
|
|
6
|
+
import mimetypes
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FileStorageManagerAsync(AsyncClient):
|
|
10
|
+
async def upload(
|
|
11
|
+
self, file_path: str, parent_id: Optional[int] = None
|
|
12
|
+
) -> Tuple[bool, Dict[str, Any]]:
|
|
13
|
+
"""
|
|
14
|
+
description:
|
|
15
|
+
上传文件(合并了初始化、分片上传、完成上传三个步骤)
|
|
16
|
+
parameters:
|
|
17
|
+
file_path: 文件路径(str)
|
|
18
|
+
parent_id: 父文件夹ID(可选)
|
|
19
|
+
return:
|
|
20
|
+
- data: 结果数据
|
|
21
|
+
- success: 是否成功
|
|
22
|
+
"""
|
|
23
|
+
# 读取文件数据
|
|
24
|
+
with open(file_path, "rb") as f:
|
|
25
|
+
file_bytes = f.read()
|
|
26
|
+
|
|
27
|
+
total_size = len(file_bytes)
|
|
28
|
+
chunk_size = 2 * 1024 * 1024 # 2MB
|
|
29
|
+
|
|
30
|
+
# 1. 初始化上传
|
|
31
|
+
file_name = os.path.basename(file_path)
|
|
32
|
+
mime_type = mimetypes.guess_type(file_path)[0]
|
|
33
|
+
init_data = {
|
|
34
|
+
"filename": file_name,
|
|
35
|
+
"file_type": mime_type,
|
|
36
|
+
"total_size": total_size,
|
|
37
|
+
}
|
|
38
|
+
if parent_id is not None:
|
|
39
|
+
init_data["parent_id"] = parent_id
|
|
40
|
+
|
|
41
|
+
init_result, success = await self._request(
|
|
42
|
+
"POST", "/api/file_storage/upload/init", json=init_data
|
|
43
|
+
)
|
|
44
|
+
if not success:
|
|
45
|
+
return {}, False
|
|
46
|
+
|
|
47
|
+
upload_id = init_result.get("upload_id")
|
|
48
|
+
chunk_urls = init_result.get("chunk_urls", [])
|
|
49
|
+
total_chunks = len(chunk_urls)
|
|
50
|
+
|
|
51
|
+
if not upload_id or not chunk_urls:
|
|
52
|
+
return {}, False
|
|
53
|
+
|
|
54
|
+
# 2. 上传所有分片
|
|
55
|
+
parts = []
|
|
56
|
+
|
|
57
|
+
async def upload_single_chunk(chunk_index: int, chunk_data: bytes):
|
|
58
|
+
"""上传单个分片"""
|
|
59
|
+
chunk_info = chunk_urls[chunk_index]
|
|
60
|
+
part_number = chunk_info.get("part_number")
|
|
61
|
+
url = chunk_info.get("url")
|
|
62
|
+
|
|
63
|
+
if not url or not part_number:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
# 使用预签名URL直接上传到OSS(PUT请求)
|
|
67
|
+
async with aiohttp.ClientSession() as session:
|
|
68
|
+
async with session.put(url, data=chunk_data) as resp:
|
|
69
|
+
if resp.status == 200:
|
|
70
|
+
etag = resp.headers.get("ETag", "").strip('"')
|
|
71
|
+
return {
|
|
72
|
+
"part_number": part_number,
|
|
73
|
+
"etag": etag,
|
|
74
|
+
"chunk_index": chunk_index,
|
|
75
|
+
}
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# 并发上传所有分片
|
|
79
|
+
tasks = []
|
|
80
|
+
for i in range(total_chunks):
|
|
81
|
+
start = i * chunk_size
|
|
82
|
+
end = min(start + chunk_size, total_size)
|
|
83
|
+
chunk_data = file_bytes[start:end]
|
|
84
|
+
tasks.append(upload_single_chunk(i, chunk_data))
|
|
85
|
+
|
|
86
|
+
# 等待所有分片上传完成,并收集结果
|
|
87
|
+
results = await asyncio.gather(*tasks)
|
|
88
|
+
|
|
89
|
+
# 检查上传结果并更新进度
|
|
90
|
+
# 按chunk_index排序,确保parts顺序正确
|
|
91
|
+
sorted_results = sorted(
|
|
92
|
+
[r for r in results if r is not None], key=lambda x: x.get("chunk_index", 0)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if len(sorted_results) != total_chunks:
|
|
96
|
+
return {}, False
|
|
97
|
+
|
|
98
|
+
for i, result in enumerate(sorted_results):
|
|
99
|
+
parts.append({"part_number": result["part_number"], "etag": result["etag"]})
|
|
100
|
+
|
|
101
|
+
# 3. 完成上传
|
|
102
|
+
complete_data = {
|
|
103
|
+
"upload_id": upload_id,
|
|
104
|
+
"parts": parts,
|
|
105
|
+
}
|
|
106
|
+
complete_result, success = await self._request(
|
|
107
|
+
"POST", "/api/file_storage/upload/complete", json=complete_data
|
|
108
|
+
)
|
|
109
|
+
if not success:
|
|
110
|
+
return {}, False
|
|
111
|
+
|
|
112
|
+
return complete_result, True
|
|
113
|
+
|
|
114
|
+
async def list_files(
|
|
115
|
+
self,
|
|
116
|
+
parent_id: Optional[int] = None,
|
|
117
|
+
is_folder: Optional[bool] = None,
|
|
118
|
+
page: int = 1,
|
|
119
|
+
page_size: int = 50,
|
|
120
|
+
) -> Tuple[List[Dict[str, Any]], bool]:
|
|
121
|
+
"""
|
|
122
|
+
description:
|
|
123
|
+
获取文件列表
|
|
124
|
+
parameters:
|
|
125
|
+
parent_id: 父文件夹ID(可选)
|
|
126
|
+
is_folder: 是否只查询文件夹(可选)
|
|
127
|
+
page: 页码(可选)
|
|
128
|
+
page_size: 每页数量(可选)
|
|
129
|
+
return:
|
|
130
|
+
- data: 文件列表数据
|
|
131
|
+
- success: 是否成功
|
|
132
|
+
"""
|
|
133
|
+
data = {
|
|
134
|
+
"page": page,
|
|
135
|
+
"page_size": page_size,
|
|
136
|
+
}
|
|
137
|
+
if parent_id is not None:
|
|
138
|
+
data["parent_id"] = parent_id
|
|
139
|
+
if is_folder is not None:
|
|
140
|
+
data["is_folder"] = is_folder
|
|
141
|
+
|
|
142
|
+
result, success = await self._request(
|
|
143
|
+
"POST", "/api/file_storage/files/list", json=data
|
|
144
|
+
)
|
|
145
|
+
if not success:
|
|
146
|
+
return {}, False
|
|
147
|
+
return result, True
|
|
148
|
+
|
|
149
|
+
async def create_folder(
|
|
150
|
+
self,
|
|
151
|
+
folder_name: str,
|
|
152
|
+
parent_id: Optional[int] = None,
|
|
153
|
+
) -> Tuple[Dict[str, Any], bool]:
|
|
154
|
+
"""
|
|
155
|
+
description:
|
|
156
|
+
创建文件夹
|
|
157
|
+
parameters:
|
|
158
|
+
folder_name: 文件夹名称
|
|
159
|
+
parent_id: 父文件夹ID(可选)
|
|
160
|
+
return:
|
|
161
|
+
- data: 文件夹数据
|
|
162
|
+
- success: 是否成功
|
|
163
|
+
"""
|
|
164
|
+
data = {
|
|
165
|
+
"folder_name": folder_name,
|
|
166
|
+
}
|
|
167
|
+
if parent_id is not None:
|
|
168
|
+
data["parent_id"] = parent_id
|
|
169
|
+
|
|
170
|
+
data, success = await self._request(
|
|
171
|
+
"POST", "/api/file_storage/files/folder/create", json=data
|
|
172
|
+
)
|
|
173
|
+
if not success:
|
|
174
|
+
return {}, False
|
|
175
|
+
return data, True
|
|
176
|
+
|
|
177
|
+
async def delete_file(
|
|
178
|
+
self,
|
|
179
|
+
record_id: int,
|
|
180
|
+
) -> Tuple[Dict[str, Any], bool]:
|
|
181
|
+
"""
|
|
182
|
+
description:
|
|
183
|
+
删除文件或文件夹
|
|
184
|
+
parameters:
|
|
185
|
+
record_id: 文件或文件夹ID
|
|
186
|
+
return:
|
|
187
|
+
- data: 结果数据
|
|
188
|
+
- success: 是否成功
|
|
189
|
+
"""
|
|
190
|
+
data, success = await self._request(
|
|
191
|
+
"DELETE", f"/api/file_storage/files/{record_id}"
|
|
192
|
+
)
|
|
193
|
+
if not success:
|
|
194
|
+
return {}, False
|
|
195
|
+
return data, True
|
|
196
|
+
|
|
197
|
+
async def get_folder_path(
|
|
198
|
+
self,
|
|
199
|
+
record_id: int,
|
|
200
|
+
) -> Tuple[List[Dict[str, Any]], bool]:
|
|
201
|
+
"""
|
|
202
|
+
description:
|
|
203
|
+
获取文件夹的完整路径
|
|
204
|
+
parameters:
|
|
205
|
+
record_id: 文件夹ID
|
|
206
|
+
return:
|
|
207
|
+
- data: 文件夹路径列表
|
|
208
|
+
- success: 是否成功
|
|
209
|
+
"""
|
|
210
|
+
data, success = await self._request(
|
|
211
|
+
"GET", f"/api/file_storage/files/{record_id}/path"
|
|
212
|
+
)
|
|
213
|
+
if not success:
|
|
214
|
+
return [], False
|
|
215
|
+
# 如果data是字典,尝试获取data字段(因为API返回的是{"data": [...]})
|
|
216
|
+
if isinstance(data, dict):
|
|
217
|
+
path_list = data.get("data", [])
|
|
218
|
+
if isinstance(path_list, list):
|
|
219
|
+
return path_list, True
|
|
220
|
+
# 如果data本身就是列表,直接返回
|
|
221
|
+
if isinstance(data, list):
|
|
222
|
+
return data, True
|
|
223
|
+
return [], False
|
|
224
|
+
|
|
225
|
+
async def generate_signed_url(
|
|
226
|
+
self,
|
|
227
|
+
record_id: int,
|
|
228
|
+
expires: int = 3600,
|
|
229
|
+
) -> Tuple[Dict[str, Any], bool]:
|
|
230
|
+
"""
|
|
231
|
+
生成签名URL(异步版本)
|
|
232
|
+
"""
|
|
233
|
+
data = {
|
|
234
|
+
"expires": expires,
|
|
235
|
+
}
|
|
236
|
+
data, success = await self._request(
|
|
237
|
+
"POST", f"/api/file_storage/files/{record_id}/generate_url", json=data
|
|
238
|
+
)
|
|
239
|
+
if not success:
|
|
240
|
+
return {}, False
|
|
241
|
+
return data, True
|
|
242
|
+
|
|
243
|
+
async def download(
|
|
244
|
+
self,
|
|
245
|
+
record_id: int,
|
|
246
|
+
save_path: str,
|
|
247
|
+
) -> Tuple[Dict[str, Any], bool]:
|
|
248
|
+
"""
|
|
249
|
+
description:
|
|
250
|
+
下载文件
|
|
251
|
+
parameters:
|
|
252
|
+
record_id: 文件记录ID
|
|
253
|
+
save_path: 保存路径
|
|
254
|
+
return:
|
|
255
|
+
- data: 下载结果数据
|
|
256
|
+
- success: 是否成功
|
|
257
|
+
"""
|
|
258
|
+
# 1. 生成签名URL
|
|
259
|
+
signed_url_data, success = await self.generate_signed_url(record_id)
|
|
260
|
+
if not success:
|
|
261
|
+
return {}, False
|
|
262
|
+
|
|
263
|
+
signed_url = signed_url_data.get("signed_url")
|
|
264
|
+
file_record = signed_url_data.get("file_record", {})
|
|
265
|
+
total_size = file_record.get("file_size", 0)
|
|
266
|
+
|
|
267
|
+
if not signed_url:
|
|
268
|
+
return {}, False
|
|
269
|
+
|
|
270
|
+
# 2. 下载文件
|
|
271
|
+
async with aiohttp.ClientSession() as session:
|
|
272
|
+
async with session.get(signed_url) as resp:
|
|
273
|
+
if resp.status != 200:
|
|
274
|
+
return {}, False
|
|
275
|
+
|
|
276
|
+
file_data = b""
|
|
277
|
+
downloaded = 0
|
|
278
|
+
|
|
279
|
+
async for chunk in resp.content.iter_chunked(8192): # 8KB chunks
|
|
280
|
+
file_data += chunk
|
|
281
|
+
downloaded += len(chunk)
|
|
282
|
+
|
|
283
|
+
with open(save_path, "wb") as f:
|
|
284
|
+
f.write(file_data)
|
|
285
|
+
|
|
286
|
+
return {"total_size": total_size, "success": True}, True
|