botrun-flow-lang 5.12.263__py3-none-any.whl → 5.12.264__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 (87) 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 +508 -508
  6. botrun_flow_lang/api/langgraph_api.py +811 -811
  7. botrun_flow_lang/api/line_bot_api.py +1484 -1484
  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 +395 -395
  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 +178 -178
  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/gemini_subsidy_graph.py +460 -460
  24. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_2_graph.py +1002 -1002
  25. botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_graph.py +822 -822
  26. botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py +723 -723
  27. botrun_flow_lang/langgraph_agents/agents/search_agent_graph.py +864 -864
  28. botrun_flow_lang/langgraph_agents/agents/tools/__init__.py +4 -4
  29. botrun_flow_lang/langgraph_agents/agents/tools/gemini_code_execution.py +376 -376
  30. botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py +66 -66
  31. botrun_flow_lang/langgraph_agents/agents/util/html_util.py +316 -316
  32. botrun_flow_lang/langgraph_agents/agents/util/img_util.py +294 -294
  33. botrun_flow_lang/langgraph_agents/agents/util/local_files.py +419 -419
  34. botrun_flow_lang/langgraph_agents/agents/util/mermaid_util.py +86 -86
  35. botrun_flow_lang/langgraph_agents/agents/util/model_utils.py +143 -143
  36. botrun_flow_lang/langgraph_agents/agents/util/pdf_analyzer.py +486 -486
  37. botrun_flow_lang/langgraph_agents/agents/util/pdf_cache.py +250 -250
  38. botrun_flow_lang/langgraph_agents/agents/util/pdf_processor.py +204 -204
  39. botrun_flow_lang/langgraph_agents/agents/util/perplexity_search.py +464 -464
  40. botrun_flow_lang/langgraph_agents/agents/util/plotly_util.py +59 -59
  41. botrun_flow_lang/langgraph_agents/agents/util/tavily_search.py +199 -199
  42. botrun_flow_lang/langgraph_agents/agents/util/youtube_util.py +90 -90
  43. botrun_flow_lang/langgraph_agents/cache/langgraph_botrun_cache.py +197 -197
  44. botrun_flow_lang/llm_agent/llm_agent.py +19 -19
  45. botrun_flow_lang/llm_agent/llm_agent_util.py +83 -83
  46. botrun_flow_lang/log/.gitignore +2 -2
  47. botrun_flow_lang/main.py +61 -61
  48. botrun_flow_lang/main_fast.py +51 -51
  49. botrun_flow_lang/mcp_server/__init__.py +10 -10
  50. botrun_flow_lang/mcp_server/default_mcp.py +744 -744
  51. botrun_flow_lang/models/nodes/utils.py +205 -205
  52. botrun_flow_lang/models/token_usage.py +34 -34
  53. botrun_flow_lang/requirements.txt +21 -21
  54. botrun_flow_lang/services/base/firestore_base.py +30 -30
  55. botrun_flow_lang/services/hatch/hatch_factory.py +11 -11
  56. botrun_flow_lang/services/hatch/hatch_fs_store.py +419 -419
  57. botrun_flow_lang/services/storage/storage_cs_store.py +206 -206
  58. botrun_flow_lang/services/storage/storage_factory.py +12 -12
  59. botrun_flow_lang/services/storage/storage_store.py +65 -65
  60. botrun_flow_lang/services/user_setting/user_setting_factory.py +9 -9
  61. botrun_flow_lang/services/user_setting/user_setting_fs_store.py +66 -66
  62. botrun_flow_lang/static/docs/tools/index.html +926 -926
  63. botrun_flow_lang/tests/api_functional_tests.py +1525 -1525
  64. botrun_flow_lang/tests/api_stress_test.py +357 -357
  65. botrun_flow_lang/tests/shared_hatch_tests.py +333 -333
  66. botrun_flow_lang/tests/test_botrun_app.py +46 -46
  67. botrun_flow_lang/tests/test_html_util.py +31 -31
  68. botrun_flow_lang/tests/test_img_analyzer.py +190 -190
  69. botrun_flow_lang/tests/test_img_util.py +39 -39
  70. botrun_flow_lang/tests/test_local_files.py +114 -114
  71. botrun_flow_lang/tests/test_mermaid_util.py +103 -103
  72. botrun_flow_lang/tests/test_pdf_analyzer.py +104 -104
  73. botrun_flow_lang/tests/test_plotly_util.py +151 -151
  74. botrun_flow_lang/tests/test_run_workflow_engine.py +65 -65
  75. botrun_flow_lang/tools/generate_docs.py +133 -133
  76. botrun_flow_lang/tools/templates/tools.html +153 -153
  77. botrun_flow_lang/utils/__init__.py +7 -7
  78. botrun_flow_lang/utils/botrun_logger.py +344 -344
  79. botrun_flow_lang/utils/clients/rate_limit_client.py +209 -209
  80. botrun_flow_lang/utils/clients/token_verify_client.py +153 -153
  81. botrun_flow_lang/utils/google_drive_utils.py +654 -654
  82. botrun_flow_lang/utils/langchain_utils.py +324 -324
  83. botrun_flow_lang/utils/yaml_utils.py +9 -9
  84. {botrun_flow_lang-5.12.263.dist-info → botrun_flow_lang-5.12.264.dist-info}/METADATA +1 -1
  85. botrun_flow_lang-5.12.264.dist-info/RECORD +102 -0
  86. botrun_flow_lang-5.12.263.dist-info/RECORD +0 -102
  87. {botrun_flow_lang-5.12.263.dist-info → botrun_flow_lang-5.12.264.dist-info}/WHEEL +0 -0
@@ -1,395 +1,395 @@
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
- async def _upload_img_file_internal(
206
- user_id: str, file_content: bytes, file_name: str, content_type: str = "image/png"
207
- ) -> str:
208
- """
209
- Internal function to upload image file to GCS
210
-
211
- Args:
212
- user_id: User ID
213
- file_content: File content as bytes
214
- file_name: File name
215
- content_type: MIME type of the file
216
-
217
- Returns:
218
- str: Public URL of the uploaded file
219
-
220
- Raises:
221
- Exception: If upload fails
222
- """
223
- storage = storage_store_factory()
224
-
225
- # Create file object from bytes
226
- file_object = BytesIO(file_content)
227
-
228
- # Build storage path - use img directory for permanent storage
229
- storage_path = f"img/{user_id}/{file_name}"
230
-
231
- # Store file with public access and content type
232
- success, public_url = await storage.store_file(
233
- storage_path, file_object, public=True, content_type=content_type
234
- )
235
-
236
- if not success:
237
- raise Exception("Failed to store image file")
238
-
239
- return public_url
240
-
241
-
242
- @router.post("/html-files/{user_id}")
243
- async def upload_html_file(
244
- user_id: str,
245
- file: FastAPIUploadFile = File(...),
246
- file_name: str = Form(...),
247
- content_type: str = Form(None),
248
- # current_user: CurrentUser = Depends(verify_jwt_token)
249
- ) -> dict:
250
- """
251
- 儲存 HTML 檔案到 GCS,檔案會是公開可存取
252
-
253
- Args:
254
- user_id: 使用者 ID
255
- file: 上傳的檔案
256
- file_name: 檔案名稱
257
- content_type: 檔案的 MIME type,如果沒有提供則使用檔案的 content_type
258
- """
259
- # Verify user permission
260
- # verify_user_permission(current_user, user_id)
261
-
262
- try:
263
- # 讀取上傳的檔案內容
264
- contents = await file.read()
265
-
266
- # 如果沒有提供 content_type,使用檔案的 content_type
267
- if not content_type:
268
- content_type = file.content_type
269
-
270
- # Use internal function to upload file
271
- public_url = await _upload_html_file_internal(
272
- user_id, contents, file_name, content_type
273
- )
274
-
275
- return {
276
- "message": "HTML file uploaded successfully",
277
- "success": True,
278
- "url": public_url,
279
- }
280
- except Exception as e:
281
- raise HTTPException(status_code=500, detail=str(e))
282
-
283
-
284
- @router.post("/img-files/{user_id}")
285
- async def upload_img_file(
286
- user_id: str,
287
- file: FastAPIUploadFile = File(...),
288
- file_name: str = Form(...),
289
- content_type: str = Form(None),
290
- # current_user: CurrentUser = Depends(verify_jwt_token)
291
- ) -> dict:
292
- """
293
- 儲存圖片檔案到 GCS,檔案會是公開可存取且永久保存
294
-
295
- Args:
296
- user_id: 使用者 ID
297
- file: 上傳的檔案
298
- file_name: 檔案名稱
299
- content_type: 檔案的 MIME type,如果沒有提供則使用檔案的 content_type
300
- """
301
- # Verify user permission
302
- # verify_user_permission(current_user, user_id)
303
-
304
- try:
305
- # 讀取上傳的檔案內容
306
- contents = await file.read()
307
-
308
- # 如果沒有提供 content_type,使用檔案的 content_type
309
- if not content_type:
310
- content_type = file.content_type
311
-
312
- # Use internal function to upload file
313
- public_url = await _upload_img_file_internal(
314
- user_id, contents, file_name, content_type
315
- )
316
-
317
- return {
318
- "message": "Image file uploaded successfully",
319
- "success": True,
320
- "url": public_url,
321
- }
322
- except Exception as e:
323
- raise HTTPException(status_code=500, detail=str(e))
324
-
325
-
326
- @router.get("/directory-sizes")
327
- async def get_directory_sizes(current_user: CurrentUser = Depends(verify_jwt_token)):
328
- """
329
- 取得 GCS bucket 中每個目錄的總檔案大小與檔案數量,排除 tmp 目錄
330
-
331
- Returns:
332
- dict: 包含每個目錄資訊的字典,包括檔案大小(bytes和人類可讀版本)和檔案數量,
333
- 並包含所有目錄的總大小和檔案總數加總在 "total" 鍵中
334
- """
335
- # Verify admin permission
336
- verify_admin_permission(current_user)
337
-
338
- try:
339
- storage = storage_store_factory()
340
- directory_info = await storage.get_directory_sizes()
341
-
342
- # 計算所有目錄的總大小和總檔案數量
343
- total_size_bytes = sum(info["size"] for info in directory_info.values())
344
- total_file_count = sum(info["file_count"] for info in directory_info.values())
345
-
346
- # 將結果轉換為更有用的格式,包含大小的人類可讀版本
347
- result = {}
348
- for directory, info in directory_info.items():
349
- size_bytes = info["size"]
350
- file_count = info["file_count"]
351
-
352
- # 轉換為適當的單位(KB, MB, GB)
353
- size_display = size_bytes
354
- unit = "bytes"
355
-
356
- if size_bytes >= 1024 * 1024 * 1024:
357
- size_display = round(size_bytes / (1024 * 1024 * 1024), 2)
358
- unit = "GB"
359
- elif size_bytes >= 1024 * 1024:
360
- size_display = round(size_bytes / (1024 * 1024), 2)
361
- unit = "MB"
362
- elif size_bytes >= 1024:
363
- size_display = round(size_bytes / 1024, 2)
364
- unit = "KB"
365
-
366
- result[directory] = {
367
- "size_bytes": size_bytes,
368
- "size_display": f"{size_display} {unit}",
369
- "file_count": file_count,
370
- }
371
-
372
- # 添加總大小的人類可讀版本
373
- total_display = total_size_bytes
374
- total_unit = "bytes"
375
-
376
- if total_size_bytes >= 1024 * 1024 * 1024:
377
- total_display = round(total_size_bytes / (1024 * 1024 * 1024), 2)
378
- total_unit = "GB"
379
- elif total_size_bytes >= 1024 * 1024:
380
- total_display = round(total_size_bytes / (1024 * 1024), 2)
381
- total_unit = "MB"
382
- elif total_size_bytes >= 1024:
383
- total_display = round(total_size_bytes / 1024, 2)
384
- total_unit = "KB"
385
-
386
- # 添加總大小和總檔案數到結果中
387
- result["total"] = {
388
- "size_bytes": total_size_bytes,
389
- "size_display": f"{total_display} {total_unit}",
390
- "file_count": total_file_count,
391
- }
392
-
393
- return result
394
- except Exception as e:
395
- 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
+ async def _upload_img_file_internal(
206
+ user_id: str, file_content: bytes, file_name: str, content_type: str = "image/png"
207
+ ) -> str:
208
+ """
209
+ Internal function to upload image file to GCS
210
+
211
+ Args:
212
+ user_id: User ID
213
+ file_content: File content as bytes
214
+ file_name: File name
215
+ content_type: MIME type of the file
216
+
217
+ Returns:
218
+ str: Public URL of the uploaded file
219
+
220
+ Raises:
221
+ Exception: If upload fails
222
+ """
223
+ storage = storage_store_factory()
224
+
225
+ # Create file object from bytes
226
+ file_object = BytesIO(file_content)
227
+
228
+ # Build storage path - use img directory for permanent storage
229
+ storage_path = f"img/{user_id}/{file_name}"
230
+
231
+ # Store file with public access and content type
232
+ success, public_url = await storage.store_file(
233
+ storage_path, file_object, public=True, content_type=content_type
234
+ )
235
+
236
+ if not success:
237
+ raise Exception("Failed to store image file")
238
+
239
+ return public_url
240
+
241
+
242
+ @router.post("/html-files/{user_id}")
243
+ async def upload_html_file(
244
+ user_id: str,
245
+ file: FastAPIUploadFile = File(...),
246
+ file_name: str = Form(...),
247
+ content_type: str = Form(None),
248
+ # current_user: CurrentUser = Depends(verify_jwt_token)
249
+ ) -> dict:
250
+ """
251
+ 儲存 HTML 檔案到 GCS,檔案會是公開可存取
252
+
253
+ Args:
254
+ user_id: 使用者 ID
255
+ file: 上傳的檔案
256
+ file_name: 檔案名稱
257
+ content_type: 檔案的 MIME type,如果沒有提供則使用檔案的 content_type
258
+ """
259
+ # Verify user permission
260
+ # verify_user_permission(current_user, user_id)
261
+
262
+ try:
263
+ # 讀取上傳的檔案內容
264
+ contents = await file.read()
265
+
266
+ # 如果沒有提供 content_type,使用檔案的 content_type
267
+ if not content_type:
268
+ content_type = file.content_type
269
+
270
+ # Use internal function to upload file
271
+ public_url = await _upload_html_file_internal(
272
+ user_id, contents, file_name, content_type
273
+ )
274
+
275
+ return {
276
+ "message": "HTML file uploaded successfully",
277
+ "success": True,
278
+ "url": public_url,
279
+ }
280
+ except Exception as e:
281
+ raise HTTPException(status_code=500, detail=str(e))
282
+
283
+
284
+ @router.post("/img-files/{user_id}")
285
+ async def upload_img_file(
286
+ user_id: str,
287
+ file: FastAPIUploadFile = File(...),
288
+ file_name: str = Form(...),
289
+ content_type: str = Form(None),
290
+ # current_user: CurrentUser = Depends(verify_jwt_token)
291
+ ) -> dict:
292
+ """
293
+ 儲存圖片檔案到 GCS,檔案會是公開可存取且永久保存
294
+
295
+ Args:
296
+ user_id: 使用者 ID
297
+ file: 上傳的檔案
298
+ file_name: 檔案名稱
299
+ content_type: 檔案的 MIME type,如果沒有提供則使用檔案的 content_type
300
+ """
301
+ # Verify user permission
302
+ # verify_user_permission(current_user, user_id)
303
+
304
+ try:
305
+ # 讀取上傳的檔案內容
306
+ contents = await file.read()
307
+
308
+ # 如果沒有提供 content_type,使用檔案的 content_type
309
+ if not content_type:
310
+ content_type = file.content_type
311
+
312
+ # Use internal function to upload file
313
+ public_url = await _upload_img_file_internal(
314
+ user_id, contents, file_name, content_type
315
+ )
316
+
317
+ return {
318
+ "message": "Image file uploaded successfully",
319
+ "success": True,
320
+ "url": public_url,
321
+ }
322
+ except Exception as e:
323
+ raise HTTPException(status_code=500, detail=str(e))
324
+
325
+
326
+ @router.get("/directory-sizes")
327
+ async def get_directory_sizes(current_user: CurrentUser = Depends(verify_jwt_token)):
328
+ """
329
+ 取得 GCS bucket 中每個目錄的總檔案大小與檔案數量,排除 tmp 目錄
330
+
331
+ Returns:
332
+ dict: 包含每個目錄資訊的字典,包括檔案大小(bytes和人類可讀版本)和檔案數量,
333
+ 並包含所有目錄的總大小和檔案總數加總在 "total" 鍵中
334
+ """
335
+ # Verify admin permission
336
+ verify_admin_permission(current_user)
337
+
338
+ try:
339
+ storage = storage_store_factory()
340
+ directory_info = await storage.get_directory_sizes()
341
+
342
+ # 計算所有目錄的總大小和總檔案數量
343
+ total_size_bytes = sum(info["size"] for info in directory_info.values())
344
+ total_file_count = sum(info["file_count"] for info in directory_info.values())
345
+
346
+ # 將結果轉換為更有用的格式,包含大小的人類可讀版本
347
+ result = {}
348
+ for directory, info in directory_info.items():
349
+ size_bytes = info["size"]
350
+ file_count = info["file_count"]
351
+
352
+ # 轉換為適當的單位(KB, MB, GB)
353
+ size_display = size_bytes
354
+ unit = "bytes"
355
+
356
+ if size_bytes >= 1024 * 1024 * 1024:
357
+ size_display = round(size_bytes / (1024 * 1024 * 1024), 2)
358
+ unit = "GB"
359
+ elif size_bytes >= 1024 * 1024:
360
+ size_display = round(size_bytes / (1024 * 1024), 2)
361
+ unit = "MB"
362
+ elif size_bytes >= 1024:
363
+ size_display = round(size_bytes / 1024, 2)
364
+ unit = "KB"
365
+
366
+ result[directory] = {
367
+ "size_bytes": size_bytes,
368
+ "size_display": f"{size_display} {unit}",
369
+ "file_count": file_count,
370
+ }
371
+
372
+ # 添加總大小的人類可讀版本
373
+ total_display = total_size_bytes
374
+ total_unit = "bytes"
375
+
376
+ if total_size_bytes >= 1024 * 1024 * 1024:
377
+ total_display = round(total_size_bytes / (1024 * 1024 * 1024), 2)
378
+ total_unit = "GB"
379
+ elif total_size_bytes >= 1024 * 1024:
380
+ total_display = round(total_size_bytes / (1024 * 1024), 2)
381
+ total_unit = "MB"
382
+ elif total_size_bytes >= 1024:
383
+ total_display = round(total_size_bytes / 1024, 2)
384
+ total_unit = "KB"
385
+
386
+ # 添加總大小和總檔案數到結果中
387
+ result["total"] = {
388
+ "size_bytes": total_size_bytes,
389
+ "size_display": f"{total_display} {total_unit}",
390
+ "file_count": total_file_count,
391
+ }
392
+
393
+ return result
394
+ except Exception as e:
395
+ raise HTTPException(status_code=500, detail=str(e))