botrun-flow-lang 5.10.32__py3-none-any.whl → 5.10.82__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.
- botrun_flow_lang/api/auth_api.py +39 -39
- botrun_flow_lang/api/auth_utils.py +183 -183
- botrun_flow_lang/api/botrun_back_api.py +65 -65
- botrun_flow_lang/api/flow_api.py +3 -3
- botrun_flow_lang/api/hatch_api.py +481 -481
- botrun_flow_lang/api/langgraph_api.py +796 -796
- botrun_flow_lang/api/line_bot_api.py +1357 -1357
- botrun_flow_lang/api/model_api.py +300 -300
- botrun_flow_lang/api/rate_limit_api.py +32 -32
- botrun_flow_lang/api/routes.py +79 -79
- botrun_flow_lang/api/search_api.py +53 -53
- botrun_flow_lang/api/storage_api.py +316 -316
- botrun_flow_lang/api/subsidy_api.py +290 -290
- botrun_flow_lang/api/subsidy_api_system_prompt.txt +109 -109
- botrun_flow_lang/api/user_setting_api.py +70 -70
- botrun_flow_lang/api/version_api.py +31 -31
- botrun_flow_lang/api/youtube_api.py +26 -26
- botrun_flow_lang/constants.py +13 -13
- botrun_flow_lang/langgraph_agents/agents/agent_runner.py +174 -174
- botrun_flow_lang/langgraph_agents/agents/agent_tools/step_planner.py +77 -77
- botrun_flow_lang/langgraph_agents/agents/checkpointer/firestore_checkpointer.py +666 -666
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/GOV_RESEARCHER_PRD.md +192 -192
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_2_graph.py +1002 -1002
- botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_graph.py +822 -822
- botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py +548 -548
- botrun_flow_lang/langgraph_agents/agents/search_agent_graph.py +864 -864
- botrun_flow_lang/langgraph_agents/agents/tools/__init__.py +4 -4
- botrun_flow_lang/langgraph_agents/agents/tools/gemini_code_execution.py +376 -376
- botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py +66 -66
- botrun_flow_lang/langgraph_agents/agents/util/html_util.py +316 -316
- botrun_flow_lang/langgraph_agents/agents/util/img_util.py +294 -294
- botrun_flow_lang/langgraph_agents/agents/util/local_files.py +345 -345
- botrun_flow_lang/langgraph_agents/agents/util/mermaid_util.py +86 -86
- botrun_flow_lang/langgraph_agents/agents/util/model_utils.py +143 -143
- botrun_flow_lang/langgraph_agents/agents/util/pdf_analyzer.py +160 -160
- botrun_flow_lang/langgraph_agents/agents/util/perplexity_search.py +464 -464
- botrun_flow_lang/langgraph_agents/agents/util/plotly_util.py +59 -59
- botrun_flow_lang/langgraph_agents/agents/util/tavily_search.py +199 -199
- botrun_flow_lang/langgraph_agents/agents/util/youtube_util.py +90 -90
- botrun_flow_lang/langgraph_agents/cache/langgraph_botrun_cache.py +197 -197
- botrun_flow_lang/llm_agent/llm_agent.py +19 -19
- botrun_flow_lang/llm_agent/llm_agent_util.py +83 -83
- botrun_flow_lang/log/.gitignore +2 -2
- botrun_flow_lang/main.py +61 -61
- botrun_flow_lang/main_fast.py +51 -51
- botrun_flow_lang/mcp_server/__init__.py +10 -10
- botrun_flow_lang/mcp_server/default_mcp.py +711 -711
- botrun_flow_lang/models/nodes/utils.py +205 -205
- botrun_flow_lang/models/token_usage.py +34 -34
- botrun_flow_lang/requirements.txt +21 -21
- botrun_flow_lang/services/base/firestore_base.py +30 -30
- botrun_flow_lang/services/hatch/hatch_factory.py +11 -11
- botrun_flow_lang/services/hatch/hatch_fs_store.py +372 -372
- botrun_flow_lang/services/storage/storage_cs_store.py +202 -202
- botrun_flow_lang/services/storage/storage_factory.py +12 -12
- botrun_flow_lang/services/storage/storage_store.py +65 -65
- botrun_flow_lang/services/user_setting/user_setting_factory.py +9 -9
- botrun_flow_lang/services/user_setting/user_setting_fs_store.py +66 -66
- botrun_flow_lang/static/docs/tools/index.html +926 -926
- botrun_flow_lang/tests/api_functional_tests.py +1525 -1525
- botrun_flow_lang/tests/api_stress_test.py +357 -357
- botrun_flow_lang/tests/shared_hatch_tests.py +333 -333
- botrun_flow_lang/tests/test_botrun_app.py +46 -46
- botrun_flow_lang/tests/test_html_util.py +31 -31
- botrun_flow_lang/tests/test_img_analyzer.py +190 -190
- botrun_flow_lang/tests/test_img_util.py +39 -39
- botrun_flow_lang/tests/test_local_files.py +114 -114
- botrun_flow_lang/tests/test_mermaid_util.py +103 -103
- botrun_flow_lang/tests/test_pdf_analyzer.py +104 -104
- botrun_flow_lang/tests/test_plotly_util.py +151 -151
- botrun_flow_lang/tests/test_run_workflow_engine.py +65 -65
- botrun_flow_lang/tools/generate_docs.py +133 -133
- botrun_flow_lang/tools/templates/tools.html +153 -153
- botrun_flow_lang/utils/__init__.py +7 -7
- botrun_flow_lang/utils/botrun_logger.py +344 -344
- botrun_flow_lang/utils/clients/rate_limit_client.py +209 -209
- botrun_flow_lang/utils/clients/token_verify_client.py +153 -153
- botrun_flow_lang/utils/google_drive_utils.py +654 -654
- botrun_flow_lang/utils/langchain_utils.py +324 -324
- botrun_flow_lang/utils/yaml_utils.py +9 -9
- {botrun_flow_lang-5.10.32.dist-info → botrun_flow_lang-5.10.82.dist-info}/METADATA +2 -2
- botrun_flow_lang-5.10.82.dist-info/RECORD +99 -0
- botrun_flow_lang-5.10.32.dist-info/RECORD +0 -99
- {botrun_flow_lang-5.10.32.dist-info → botrun_flow_lang-5.10.82.dist-info}/WHEEL +0 -0
|
@@ -1,316 +1,316 @@
|
|
|
1
|
-
from dotenv import load_dotenv
|
|
2
|
-
from fastapi import (
|
|
3
|
-
APIRouter,
|
|
4
|
-
UploadFile as FastAPIUploadFile,
|
|
5
|
-
File,
|
|
6
|
-
HTTPException,
|
|
7
|
-
Form,
|
|
8
|
-
Depends,
|
|
9
|
-
)
|
|
10
|
-
from typing import Optional
|
|
11
|
-
from io import BytesIO
|
|
12
|
-
import json
|
|
13
|
-
|
|
14
|
-
from botrun_hatch.models.upload_file import UploadFile
|
|
15
|
-
from botrun_flow_lang.services.storage.storage_factory import storage_store_factory
|
|
16
|
-
from fastapi.responses import StreamingResponse
|
|
17
|
-
from botrun_flow_lang.api.auth_utils import (
|
|
18
|
-
verify_jwt_token,
|
|
19
|
-
verify_user_permission,
|
|
20
|
-
verify_admin_permission,
|
|
21
|
-
CurrentUser,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
router = APIRouter()
|
|
25
|
-
load_dotenv()
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@router.post("/files/{user_id}")
|
|
29
|
-
async def upload_file(
|
|
30
|
-
user_id: str,
|
|
31
|
-
file: FastAPIUploadFile = File(...),
|
|
32
|
-
file_info: str = Form(...),
|
|
33
|
-
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
34
|
-
) -> dict:
|
|
35
|
-
"""
|
|
36
|
-
儲存檔案到 GCS
|
|
37
|
-
"""
|
|
38
|
-
# Verify user permission
|
|
39
|
-
verify_user_permission(current_user, user_id)
|
|
40
|
-
|
|
41
|
-
try:
|
|
42
|
-
# 解析 file_info JSON 字串
|
|
43
|
-
file_info_dict = json.loads(file_info)
|
|
44
|
-
file_info_obj = UploadFile(**file_info_dict)
|
|
45
|
-
|
|
46
|
-
storage = storage_store_factory()
|
|
47
|
-
|
|
48
|
-
# 讀取上傳的檔案內容
|
|
49
|
-
contents = await file.read()
|
|
50
|
-
file_object = BytesIO(contents)
|
|
51
|
-
|
|
52
|
-
# 構建存儲路徑
|
|
53
|
-
storage_path = f"{user_id}/{file_info_obj.id}"
|
|
54
|
-
|
|
55
|
-
# 存儲檔案
|
|
56
|
-
success = await storage.store_file(storage_path, file_object)
|
|
57
|
-
if not success:
|
|
58
|
-
raise HTTPException(status_code=500, detail="Failed to store file")
|
|
59
|
-
|
|
60
|
-
return {"message": "File uploaded successfully", "success": True}
|
|
61
|
-
except json.JSONDecodeError:
|
|
62
|
-
raise HTTPException(status_code=400, detail="Invalid JSON format for file_info")
|
|
63
|
-
except Exception as e:
|
|
64
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@router.get("/files/{user_id}/{file_id}", response_class=StreamingResponse)
|
|
68
|
-
async def get_file(
|
|
69
|
-
user_id: str, file_id: str, current_user: CurrentUser = Depends(verify_jwt_token)
|
|
70
|
-
):
|
|
71
|
-
"""
|
|
72
|
-
從 GCS 取得檔案
|
|
73
|
-
"""
|
|
74
|
-
# Verify user permission
|
|
75
|
-
verify_user_permission(current_user, user_id)
|
|
76
|
-
|
|
77
|
-
try:
|
|
78
|
-
storage = storage_store_factory()
|
|
79
|
-
storage_path = f"{user_id}/{file_id}"
|
|
80
|
-
|
|
81
|
-
file_object = await storage.retrieve_file(storage_path)
|
|
82
|
-
if not file_object:
|
|
83
|
-
raise HTTPException(status_code=404, detail="File not found")
|
|
84
|
-
|
|
85
|
-
return StreamingResponse(
|
|
86
|
-
iter([file_object.getvalue()]), media_type="application/octet-stream"
|
|
87
|
-
)
|
|
88
|
-
except Exception as e:
|
|
89
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@router.delete("/files/{user_id}/{file_id}")
|
|
93
|
-
async def delete_file(
|
|
94
|
-
user_id: str, file_id: str, current_user: CurrentUser = Depends(verify_jwt_token)
|
|
95
|
-
):
|
|
96
|
-
"""
|
|
97
|
-
從 GCS 刪除檔案
|
|
98
|
-
"""
|
|
99
|
-
# Verify user permission
|
|
100
|
-
verify_user_permission(current_user, user_id)
|
|
101
|
-
|
|
102
|
-
try:
|
|
103
|
-
storage = storage_store_factory()
|
|
104
|
-
storage_path = f"{user_id}/{file_id}"
|
|
105
|
-
|
|
106
|
-
success = await storage.delete_file(storage_path)
|
|
107
|
-
if not success:
|
|
108
|
-
raise HTTPException(
|
|
109
|
-
status_code=404, detail="File not found or could not be deleted"
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
return {"message": "File deleted successfully", "success": True}
|
|
113
|
-
except Exception as e:
|
|
114
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
@router.post("/tmp-files/{user_id}")
|
|
118
|
-
async def upload_tmp_file(
|
|
119
|
-
user_id: str,
|
|
120
|
-
file: FastAPIUploadFile = File(...),
|
|
121
|
-
file_name: str = Form(...),
|
|
122
|
-
content_type: str = Form(None),
|
|
123
|
-
# current_user: CurrentUser = Depends(verify_jwt_token),
|
|
124
|
-
) -> dict:
|
|
125
|
-
"""
|
|
126
|
-
儲存暫存檔案到 GCS,檔案會是公開可存取且有 7 天的生命週期
|
|
127
|
-
|
|
128
|
-
Args:
|
|
129
|
-
user_id: 使用者 ID
|
|
130
|
-
file: 上傳的檔案
|
|
131
|
-
file_name: 檔案名稱
|
|
132
|
-
content_type: 檔案的 MIME type,如果沒有提供則使用檔案的 content_type
|
|
133
|
-
"""
|
|
134
|
-
# Verify user permission
|
|
135
|
-
# verify_user_permission(current_user, user_id)
|
|
136
|
-
|
|
137
|
-
try:
|
|
138
|
-
storage = storage_store_factory()
|
|
139
|
-
|
|
140
|
-
# 讀取上傳的檔案內容
|
|
141
|
-
contents = await file.read()
|
|
142
|
-
file_object = BytesIO(contents)
|
|
143
|
-
|
|
144
|
-
# 如果沒有提供 content_type,使用檔案的 content_type
|
|
145
|
-
if not content_type:
|
|
146
|
-
content_type = file.content_type
|
|
147
|
-
|
|
148
|
-
# 構建存儲路徑 - 使用 tmp 目錄來區分暫存檔案
|
|
149
|
-
storage_path = f"tmp/{user_id}/{file_name}"
|
|
150
|
-
|
|
151
|
-
# 存儲檔案,設定為公開存取,並傳入 content_type
|
|
152
|
-
success, public_url = await storage.store_file(
|
|
153
|
-
storage_path, file_object, public=True, content_type=content_type
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
if not success:
|
|
157
|
-
raise HTTPException(status_code=500, detail="Failed to store file")
|
|
158
|
-
|
|
159
|
-
return {
|
|
160
|
-
"message": "Temporary file uploaded successfully",
|
|
161
|
-
"success": True,
|
|
162
|
-
"url": public_url,
|
|
163
|
-
}
|
|
164
|
-
except Exception as e:
|
|
165
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
async def _upload_html_file_internal(
|
|
169
|
-
user_id: str, file_content: bytes, file_name: str, content_type: str = "text/html"
|
|
170
|
-
) -> str:
|
|
171
|
-
"""
|
|
172
|
-
Internal function to upload HTML file to GCS
|
|
173
|
-
|
|
174
|
-
Args:
|
|
175
|
-
user_id: User ID
|
|
176
|
-
file_content: File content as bytes
|
|
177
|
-
file_name: File name
|
|
178
|
-
content_type: MIME type of the file
|
|
179
|
-
|
|
180
|
-
Returns:
|
|
181
|
-
str: Public URL of the uploaded file
|
|
182
|
-
|
|
183
|
-
Raises:
|
|
184
|
-
Exception: If upload fails
|
|
185
|
-
"""
|
|
186
|
-
storage = storage_store_factory()
|
|
187
|
-
|
|
188
|
-
# Create file object from bytes
|
|
189
|
-
file_object = BytesIO(file_content)
|
|
190
|
-
|
|
191
|
-
# Build storage path - use html directory
|
|
192
|
-
storage_path = f"html/{user_id}/{file_name}"
|
|
193
|
-
|
|
194
|
-
# Store file with public access and content type
|
|
195
|
-
success, public_url = await storage.store_file(
|
|
196
|
-
storage_path, file_object, public=True, content_type=content_type
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
if not success:
|
|
200
|
-
raise Exception("Failed to store file")
|
|
201
|
-
|
|
202
|
-
return public_url
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
@router.post("/html-files/{user_id}")
|
|
206
|
-
async def upload_html_file(
|
|
207
|
-
user_id: str,
|
|
208
|
-
file: FastAPIUploadFile = File(...),
|
|
209
|
-
file_name: str = Form(...),
|
|
210
|
-
content_type: str = Form(None),
|
|
211
|
-
# current_user: CurrentUser = Depends(verify_jwt_token)
|
|
212
|
-
) -> dict:
|
|
213
|
-
"""
|
|
214
|
-
儲存 HTML 檔案到 GCS,檔案會是公開可存取
|
|
215
|
-
|
|
216
|
-
Args:
|
|
217
|
-
user_id: 使用者 ID
|
|
218
|
-
file: 上傳的檔案
|
|
219
|
-
file_name: 檔案名稱
|
|
220
|
-
content_type: 檔案的 MIME type,如果沒有提供則使用檔案的 content_type
|
|
221
|
-
"""
|
|
222
|
-
# Verify user permission
|
|
223
|
-
# verify_user_permission(current_user, user_id)
|
|
224
|
-
|
|
225
|
-
try:
|
|
226
|
-
# 讀取上傳的檔案內容
|
|
227
|
-
contents = await file.read()
|
|
228
|
-
|
|
229
|
-
# 如果沒有提供 content_type,使用檔案的 content_type
|
|
230
|
-
if not content_type:
|
|
231
|
-
content_type = file.content_type
|
|
232
|
-
|
|
233
|
-
# Use internal function to upload file
|
|
234
|
-
public_url = await _upload_html_file_internal(
|
|
235
|
-
user_id, contents, file_name, content_type
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
return {
|
|
239
|
-
"message": "HTML file uploaded successfully",
|
|
240
|
-
"success": True,
|
|
241
|
-
"url": public_url,
|
|
242
|
-
}
|
|
243
|
-
except Exception as e:
|
|
244
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
@router.get("/directory-sizes")
|
|
248
|
-
async def get_directory_sizes(current_user: CurrentUser = Depends(verify_jwt_token)):
|
|
249
|
-
"""
|
|
250
|
-
取得 GCS bucket 中每個目錄的總檔案大小與檔案數量,排除 tmp 目錄
|
|
251
|
-
|
|
252
|
-
Returns:
|
|
253
|
-
dict: 包含每個目錄資訊的字典,包括檔案大小(bytes和人類可讀版本)和檔案數量,
|
|
254
|
-
並包含所有目錄的總大小和檔案總數加總在 "total" 鍵中
|
|
255
|
-
"""
|
|
256
|
-
# Verify admin permission
|
|
257
|
-
verify_admin_permission(current_user)
|
|
258
|
-
|
|
259
|
-
try:
|
|
260
|
-
storage = storage_store_factory()
|
|
261
|
-
directory_info = await storage.get_directory_sizes()
|
|
262
|
-
|
|
263
|
-
# 計算所有目錄的總大小和總檔案數量
|
|
264
|
-
total_size_bytes = sum(info["size"] for info in directory_info.values())
|
|
265
|
-
total_file_count = sum(info["file_count"] for info in directory_info.values())
|
|
266
|
-
|
|
267
|
-
# 將結果轉換為更有用的格式,包含大小的人類可讀版本
|
|
268
|
-
result = {}
|
|
269
|
-
for directory, info in directory_info.items():
|
|
270
|
-
size_bytes = info["size"]
|
|
271
|
-
file_count = info["file_count"]
|
|
272
|
-
|
|
273
|
-
# 轉換為適當的單位(KB, MB, GB)
|
|
274
|
-
size_display = size_bytes
|
|
275
|
-
unit = "bytes"
|
|
276
|
-
|
|
277
|
-
if size_bytes >= 1024 * 1024 * 1024:
|
|
278
|
-
size_display = round(size_bytes / (1024 * 1024 * 1024), 2)
|
|
279
|
-
unit = "GB"
|
|
280
|
-
elif size_bytes >= 1024 * 1024:
|
|
281
|
-
size_display = round(size_bytes / (1024 * 1024), 2)
|
|
282
|
-
unit = "MB"
|
|
283
|
-
elif size_bytes >= 1024:
|
|
284
|
-
size_display = round(size_bytes / 1024, 2)
|
|
285
|
-
unit = "KB"
|
|
286
|
-
|
|
287
|
-
result[directory] = {
|
|
288
|
-
"size_bytes": size_bytes,
|
|
289
|
-
"size_display": f"{size_display} {unit}",
|
|
290
|
-
"file_count": file_count,
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
# 添加總大小的人類可讀版本
|
|
294
|
-
total_display = total_size_bytes
|
|
295
|
-
total_unit = "bytes"
|
|
296
|
-
|
|
297
|
-
if total_size_bytes >= 1024 * 1024 * 1024:
|
|
298
|
-
total_display = round(total_size_bytes / (1024 * 1024 * 1024), 2)
|
|
299
|
-
total_unit = "GB"
|
|
300
|
-
elif total_size_bytes >= 1024 * 1024:
|
|
301
|
-
total_display = round(total_size_bytes / (1024 * 1024), 2)
|
|
302
|
-
total_unit = "MB"
|
|
303
|
-
elif total_size_bytes >= 1024:
|
|
304
|
-
total_display = round(total_size_bytes / 1024, 2)
|
|
305
|
-
total_unit = "KB"
|
|
306
|
-
|
|
307
|
-
# 添加總大小和總檔案數到結果中
|
|
308
|
-
result["total"] = {
|
|
309
|
-
"size_bytes": total_size_bytes,
|
|
310
|
-
"size_display": f"{total_display} {total_unit}",
|
|
311
|
-
"file_count": total_file_count,
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return result
|
|
315
|
-
except Exception as e:
|
|
316
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
1
|
+
from dotenv import load_dotenv
|
|
2
|
+
from fastapi import (
|
|
3
|
+
APIRouter,
|
|
4
|
+
UploadFile as FastAPIUploadFile,
|
|
5
|
+
File,
|
|
6
|
+
HTTPException,
|
|
7
|
+
Form,
|
|
8
|
+
Depends,
|
|
9
|
+
)
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from io import BytesIO
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
from botrun_hatch.models.upload_file import UploadFile
|
|
15
|
+
from botrun_flow_lang.services.storage.storage_factory import storage_store_factory
|
|
16
|
+
from fastapi.responses import StreamingResponse
|
|
17
|
+
from botrun_flow_lang.api.auth_utils import (
|
|
18
|
+
verify_jwt_token,
|
|
19
|
+
verify_user_permission,
|
|
20
|
+
verify_admin_permission,
|
|
21
|
+
CurrentUser,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
router = APIRouter()
|
|
25
|
+
load_dotenv()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@router.post("/files/{user_id}")
|
|
29
|
+
async def upload_file(
|
|
30
|
+
user_id: str,
|
|
31
|
+
file: FastAPIUploadFile = File(...),
|
|
32
|
+
file_info: str = Form(...),
|
|
33
|
+
current_user: CurrentUser = Depends(verify_jwt_token),
|
|
34
|
+
) -> dict:
|
|
35
|
+
"""
|
|
36
|
+
儲存檔案到 GCS
|
|
37
|
+
"""
|
|
38
|
+
# Verify user permission
|
|
39
|
+
verify_user_permission(current_user, user_id)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
# 解析 file_info JSON 字串
|
|
43
|
+
file_info_dict = json.loads(file_info)
|
|
44
|
+
file_info_obj = UploadFile(**file_info_dict)
|
|
45
|
+
|
|
46
|
+
storage = storage_store_factory()
|
|
47
|
+
|
|
48
|
+
# 讀取上傳的檔案內容
|
|
49
|
+
contents = await file.read()
|
|
50
|
+
file_object = BytesIO(contents)
|
|
51
|
+
|
|
52
|
+
# 構建存儲路徑
|
|
53
|
+
storage_path = f"{user_id}/{file_info_obj.id}"
|
|
54
|
+
|
|
55
|
+
# 存儲檔案
|
|
56
|
+
success = await storage.store_file(storage_path, file_object)
|
|
57
|
+
if not success:
|
|
58
|
+
raise HTTPException(status_code=500, detail="Failed to store file")
|
|
59
|
+
|
|
60
|
+
return {"message": "File uploaded successfully", "success": True}
|
|
61
|
+
except json.JSONDecodeError:
|
|
62
|
+
raise HTTPException(status_code=400, detail="Invalid JSON format for file_info")
|
|
63
|
+
except Exception as e:
|
|
64
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@router.get("/files/{user_id}/{file_id}", response_class=StreamingResponse)
|
|
68
|
+
async def get_file(
|
|
69
|
+
user_id: str, file_id: str, current_user: CurrentUser = Depends(verify_jwt_token)
|
|
70
|
+
):
|
|
71
|
+
"""
|
|
72
|
+
從 GCS 取得檔案
|
|
73
|
+
"""
|
|
74
|
+
# Verify user permission
|
|
75
|
+
verify_user_permission(current_user, user_id)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
storage = storage_store_factory()
|
|
79
|
+
storage_path = f"{user_id}/{file_id}"
|
|
80
|
+
|
|
81
|
+
file_object = await storage.retrieve_file(storage_path)
|
|
82
|
+
if not file_object:
|
|
83
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
84
|
+
|
|
85
|
+
return StreamingResponse(
|
|
86
|
+
iter([file_object.getvalue()]), media_type="application/octet-stream"
|
|
87
|
+
)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@router.delete("/files/{user_id}/{file_id}")
|
|
93
|
+
async def delete_file(
|
|
94
|
+
user_id: str, file_id: str, current_user: CurrentUser = Depends(verify_jwt_token)
|
|
95
|
+
):
|
|
96
|
+
"""
|
|
97
|
+
從 GCS 刪除檔案
|
|
98
|
+
"""
|
|
99
|
+
# Verify user permission
|
|
100
|
+
verify_user_permission(current_user, user_id)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
storage = storage_store_factory()
|
|
104
|
+
storage_path = f"{user_id}/{file_id}"
|
|
105
|
+
|
|
106
|
+
success = await storage.delete_file(storage_path)
|
|
107
|
+
if not success:
|
|
108
|
+
raise HTTPException(
|
|
109
|
+
status_code=404, detail="File not found or could not be deleted"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return {"message": "File deleted successfully", "success": True}
|
|
113
|
+
except Exception as e:
|
|
114
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@router.post("/tmp-files/{user_id}")
|
|
118
|
+
async def upload_tmp_file(
|
|
119
|
+
user_id: str,
|
|
120
|
+
file: FastAPIUploadFile = File(...),
|
|
121
|
+
file_name: str = Form(...),
|
|
122
|
+
content_type: str = Form(None),
|
|
123
|
+
# current_user: CurrentUser = Depends(verify_jwt_token),
|
|
124
|
+
) -> dict:
|
|
125
|
+
"""
|
|
126
|
+
儲存暫存檔案到 GCS,檔案會是公開可存取且有 7 天的生命週期
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
user_id: 使用者 ID
|
|
130
|
+
file: 上傳的檔案
|
|
131
|
+
file_name: 檔案名稱
|
|
132
|
+
content_type: 檔案的 MIME type,如果沒有提供則使用檔案的 content_type
|
|
133
|
+
"""
|
|
134
|
+
# Verify user permission
|
|
135
|
+
# verify_user_permission(current_user, user_id)
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
storage = storage_store_factory()
|
|
139
|
+
|
|
140
|
+
# 讀取上傳的檔案內容
|
|
141
|
+
contents = await file.read()
|
|
142
|
+
file_object = BytesIO(contents)
|
|
143
|
+
|
|
144
|
+
# 如果沒有提供 content_type,使用檔案的 content_type
|
|
145
|
+
if not content_type:
|
|
146
|
+
content_type = file.content_type
|
|
147
|
+
|
|
148
|
+
# 構建存儲路徑 - 使用 tmp 目錄來區分暫存檔案
|
|
149
|
+
storage_path = f"tmp/{user_id}/{file_name}"
|
|
150
|
+
|
|
151
|
+
# 存儲檔案,設定為公開存取,並傳入 content_type
|
|
152
|
+
success, public_url = await storage.store_file(
|
|
153
|
+
storage_path, file_object, public=True, content_type=content_type
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if not success:
|
|
157
|
+
raise HTTPException(status_code=500, detail="Failed to store file")
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
"message": "Temporary file uploaded successfully",
|
|
161
|
+
"success": True,
|
|
162
|
+
"url": public_url,
|
|
163
|
+
}
|
|
164
|
+
except Exception as e:
|
|
165
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
async def _upload_html_file_internal(
|
|
169
|
+
user_id: str, file_content: bytes, file_name: str, content_type: str = "text/html"
|
|
170
|
+
) -> str:
|
|
171
|
+
"""
|
|
172
|
+
Internal function to upload HTML file to GCS
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
user_id: User ID
|
|
176
|
+
file_content: File content as bytes
|
|
177
|
+
file_name: File name
|
|
178
|
+
content_type: MIME type of the file
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
str: Public URL of the uploaded file
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
Exception: If upload fails
|
|
185
|
+
"""
|
|
186
|
+
storage = storage_store_factory()
|
|
187
|
+
|
|
188
|
+
# Create file object from bytes
|
|
189
|
+
file_object = BytesIO(file_content)
|
|
190
|
+
|
|
191
|
+
# Build storage path - use html directory
|
|
192
|
+
storage_path = f"html/{user_id}/{file_name}"
|
|
193
|
+
|
|
194
|
+
# Store file with public access and content type
|
|
195
|
+
success, public_url = await storage.store_file(
|
|
196
|
+
storage_path, file_object, public=True, content_type=content_type
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if not success:
|
|
200
|
+
raise Exception("Failed to store file")
|
|
201
|
+
|
|
202
|
+
return public_url
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@router.post("/html-files/{user_id}")
|
|
206
|
+
async def upload_html_file(
|
|
207
|
+
user_id: str,
|
|
208
|
+
file: FastAPIUploadFile = File(...),
|
|
209
|
+
file_name: str = Form(...),
|
|
210
|
+
content_type: str = Form(None),
|
|
211
|
+
# current_user: CurrentUser = Depends(verify_jwt_token)
|
|
212
|
+
) -> dict:
|
|
213
|
+
"""
|
|
214
|
+
儲存 HTML 檔案到 GCS,檔案會是公開可存取
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
user_id: 使用者 ID
|
|
218
|
+
file: 上傳的檔案
|
|
219
|
+
file_name: 檔案名稱
|
|
220
|
+
content_type: 檔案的 MIME type,如果沒有提供則使用檔案的 content_type
|
|
221
|
+
"""
|
|
222
|
+
# Verify user permission
|
|
223
|
+
# verify_user_permission(current_user, user_id)
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
# 讀取上傳的檔案內容
|
|
227
|
+
contents = await file.read()
|
|
228
|
+
|
|
229
|
+
# 如果沒有提供 content_type,使用檔案的 content_type
|
|
230
|
+
if not content_type:
|
|
231
|
+
content_type = file.content_type
|
|
232
|
+
|
|
233
|
+
# Use internal function to upload file
|
|
234
|
+
public_url = await _upload_html_file_internal(
|
|
235
|
+
user_id, contents, file_name, content_type
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
"message": "HTML file uploaded successfully",
|
|
240
|
+
"success": True,
|
|
241
|
+
"url": public_url,
|
|
242
|
+
}
|
|
243
|
+
except Exception as e:
|
|
244
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@router.get("/directory-sizes")
|
|
248
|
+
async def get_directory_sizes(current_user: CurrentUser = Depends(verify_jwt_token)):
|
|
249
|
+
"""
|
|
250
|
+
取得 GCS bucket 中每個目錄的總檔案大小與檔案數量,排除 tmp 目錄
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
dict: 包含每個目錄資訊的字典,包括檔案大小(bytes和人類可讀版本)和檔案數量,
|
|
254
|
+
並包含所有目錄的總大小和檔案總數加總在 "total" 鍵中
|
|
255
|
+
"""
|
|
256
|
+
# Verify admin permission
|
|
257
|
+
verify_admin_permission(current_user)
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
storage = storage_store_factory()
|
|
261
|
+
directory_info = await storage.get_directory_sizes()
|
|
262
|
+
|
|
263
|
+
# 計算所有目錄的總大小和總檔案數量
|
|
264
|
+
total_size_bytes = sum(info["size"] for info in directory_info.values())
|
|
265
|
+
total_file_count = sum(info["file_count"] for info in directory_info.values())
|
|
266
|
+
|
|
267
|
+
# 將結果轉換為更有用的格式,包含大小的人類可讀版本
|
|
268
|
+
result = {}
|
|
269
|
+
for directory, info in directory_info.items():
|
|
270
|
+
size_bytes = info["size"]
|
|
271
|
+
file_count = info["file_count"]
|
|
272
|
+
|
|
273
|
+
# 轉換為適當的單位(KB, MB, GB)
|
|
274
|
+
size_display = size_bytes
|
|
275
|
+
unit = "bytes"
|
|
276
|
+
|
|
277
|
+
if size_bytes >= 1024 * 1024 * 1024:
|
|
278
|
+
size_display = round(size_bytes / (1024 * 1024 * 1024), 2)
|
|
279
|
+
unit = "GB"
|
|
280
|
+
elif size_bytes >= 1024 * 1024:
|
|
281
|
+
size_display = round(size_bytes / (1024 * 1024), 2)
|
|
282
|
+
unit = "MB"
|
|
283
|
+
elif size_bytes >= 1024:
|
|
284
|
+
size_display = round(size_bytes / 1024, 2)
|
|
285
|
+
unit = "KB"
|
|
286
|
+
|
|
287
|
+
result[directory] = {
|
|
288
|
+
"size_bytes": size_bytes,
|
|
289
|
+
"size_display": f"{size_display} {unit}",
|
|
290
|
+
"file_count": file_count,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# 添加總大小的人類可讀版本
|
|
294
|
+
total_display = total_size_bytes
|
|
295
|
+
total_unit = "bytes"
|
|
296
|
+
|
|
297
|
+
if total_size_bytes >= 1024 * 1024 * 1024:
|
|
298
|
+
total_display = round(total_size_bytes / (1024 * 1024 * 1024), 2)
|
|
299
|
+
total_unit = "GB"
|
|
300
|
+
elif total_size_bytes >= 1024 * 1024:
|
|
301
|
+
total_display = round(total_size_bytes / (1024 * 1024), 2)
|
|
302
|
+
total_unit = "MB"
|
|
303
|
+
elif total_size_bytes >= 1024:
|
|
304
|
+
total_display = round(total_size_bytes / 1024, 2)
|
|
305
|
+
total_unit = "KB"
|
|
306
|
+
|
|
307
|
+
# 添加總大小和總檔案數到結果中
|
|
308
|
+
result["total"] = {
|
|
309
|
+
"size_bytes": total_size_bytes,
|
|
310
|
+
"size_display": f"{total_display} {total_unit}",
|
|
311
|
+
"file_count": total_file_count,
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return result
|
|
315
|
+
except Exception as e:
|
|
316
|
+
raise HTTPException(status_code=500, detail=str(e))
|