botrun-flow-lang 5.9.301__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.
Files changed (84) hide show
  1. botrun_flow_lang/api/auth_api.py +39 -39
  2. botrun_flow_lang/api/auth_utils.py +183 -183
  3. botrun_flow_lang/api/botrun_back_api.py +65 -65
  4. botrun_flow_lang/api/flow_api.py +3 -3
  5. botrun_flow_lang/api/hatch_api.py +481 -481
  6. botrun_flow_lang/api/langgraph_api.py +796 -796
  7. botrun_flow_lang/api/line_bot_api.py +1357 -1357
  8. botrun_flow_lang/api/model_api.py +300 -300
  9. botrun_flow_lang/api/rate_limit_api.py +32 -32
  10. botrun_flow_lang/api/routes.py +79 -79
  11. botrun_flow_lang/api/search_api.py +53 -53
  12. botrun_flow_lang/api/storage_api.py +316 -316
  13. botrun_flow_lang/api/subsidy_api.py +290 -290
  14. botrun_flow_lang/api/subsidy_api_system_prompt.txt +109 -109
  15. botrun_flow_lang/api/user_setting_api.py +70 -70
  16. botrun_flow_lang/api/version_api.py +31 -31
  17. botrun_flow_lang/api/youtube_api.py +26 -26
  18. botrun_flow_lang/constants.py +13 -13
  19. botrun_flow_lang/langgraph_agents/agents/agent_runner.py +174 -174
  20. botrun_flow_lang/langgraph_agents/agents/agent_tools/step_planner.py +77 -77
  21. botrun_flow_lang/langgraph_agents/agents/checkpointer/firestore_checkpointer.py +666 -666
  22. botrun_flow_lang/langgraph_agents/agents/gov_researcher/GOV_RESEARCHER_PRD.md +192 -192
  23. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_2_graph.py +1002 -1002
  24. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_graph.py +822 -822
  25. botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py +548 -542
  26. botrun_flow_lang/langgraph_agents/agents/search_agent_graph.py +864 -864
  27. botrun_flow_lang/langgraph_agents/agents/tools/__init__.py +4 -4
  28. botrun_flow_lang/langgraph_agents/agents/tools/gemini_code_execution.py +376 -376
  29. botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py +66 -66
  30. botrun_flow_lang/langgraph_agents/agents/util/html_util.py +316 -316
  31. botrun_flow_lang/langgraph_agents/agents/util/img_util.py +294 -294
  32. botrun_flow_lang/langgraph_agents/agents/util/local_files.py +345 -345
  33. botrun_flow_lang/langgraph_agents/agents/util/mermaid_util.py +86 -86
  34. botrun_flow_lang/langgraph_agents/agents/util/model_utils.py +143 -143
  35. botrun_flow_lang/langgraph_agents/agents/util/pdf_analyzer.py +160 -160
  36. botrun_flow_lang/langgraph_agents/agents/util/perplexity_search.py +464 -464
  37. botrun_flow_lang/langgraph_agents/agents/util/plotly_util.py +59 -59
  38. botrun_flow_lang/langgraph_agents/agents/util/tavily_search.py +199 -199
  39. botrun_flow_lang/langgraph_agents/agents/util/youtube_util.py +90 -90
  40. botrun_flow_lang/langgraph_agents/cache/langgraph_botrun_cache.py +197 -197
  41. botrun_flow_lang/llm_agent/llm_agent.py +19 -19
  42. botrun_flow_lang/llm_agent/llm_agent_util.py +83 -83
  43. botrun_flow_lang/log/.gitignore +2 -2
  44. botrun_flow_lang/main.py +61 -61
  45. botrun_flow_lang/main_fast.py +51 -51
  46. botrun_flow_lang/mcp_server/__init__.py +10 -10
  47. botrun_flow_lang/mcp_server/default_mcp.py +711 -711
  48. botrun_flow_lang/models/nodes/utils.py +205 -205
  49. botrun_flow_lang/models/token_usage.py +34 -34
  50. botrun_flow_lang/requirements.txt +21 -21
  51. botrun_flow_lang/services/base/firestore_base.py +30 -30
  52. botrun_flow_lang/services/hatch/hatch_factory.py +11 -11
  53. botrun_flow_lang/services/hatch/hatch_fs_store.py +372 -372
  54. botrun_flow_lang/services/storage/storage_cs_store.py +202 -202
  55. botrun_flow_lang/services/storage/storage_factory.py +12 -12
  56. botrun_flow_lang/services/storage/storage_store.py +65 -65
  57. botrun_flow_lang/services/user_setting/user_setting_factory.py +9 -9
  58. botrun_flow_lang/services/user_setting/user_setting_fs_store.py +66 -66
  59. botrun_flow_lang/static/docs/tools/index.html +926 -926
  60. botrun_flow_lang/tests/api_functional_tests.py +1525 -1525
  61. botrun_flow_lang/tests/api_stress_test.py +357 -357
  62. botrun_flow_lang/tests/shared_hatch_tests.py +333 -333
  63. botrun_flow_lang/tests/test_botrun_app.py +46 -46
  64. botrun_flow_lang/tests/test_html_util.py +31 -31
  65. botrun_flow_lang/tests/test_img_analyzer.py +190 -190
  66. botrun_flow_lang/tests/test_img_util.py +39 -39
  67. botrun_flow_lang/tests/test_local_files.py +114 -114
  68. botrun_flow_lang/tests/test_mermaid_util.py +103 -103
  69. botrun_flow_lang/tests/test_pdf_analyzer.py +104 -104
  70. botrun_flow_lang/tests/test_plotly_util.py +151 -151
  71. botrun_flow_lang/tests/test_run_workflow_engine.py +65 -65
  72. botrun_flow_lang/tools/generate_docs.py +133 -133
  73. botrun_flow_lang/tools/templates/tools.html +153 -153
  74. botrun_flow_lang/utils/__init__.py +7 -7
  75. botrun_flow_lang/utils/botrun_logger.py +344 -344
  76. botrun_flow_lang/utils/clients/rate_limit_client.py +209 -209
  77. botrun_flow_lang/utils/clients/token_verify_client.py +153 -153
  78. botrun_flow_lang/utils/google_drive_utils.py +654 -654
  79. botrun_flow_lang/utils/langchain_utils.py +324 -324
  80. botrun_flow_lang/utils/yaml_utils.py +9 -9
  81. {botrun_flow_lang-5.9.301.dist-info → botrun_flow_lang-5.10.82.dist-info}/METADATA +2 -2
  82. botrun_flow_lang-5.10.82.dist-info/RECORD +99 -0
  83. botrun_flow_lang-5.9.301.dist-info/RECORD +0 -99
  84. {botrun_flow_lang-5.9.301.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))