4040 ValidationStatus ,
4141)
4242from server .project_manager import ProjectManager
43+ from server .routers .cloud import router as cloud_router
4344from server .routers .projects import router as projects_router
4445from ifc_validator .standards .resolver import get_bundled_ids
4546
@@ -79,6 +80,7 @@ def format(self, record: logging.LogRecord) -> str:
7980
8081# Include persistent project router (PostgreSQL-backed)
8182app .include_router (projects_router )
83+ app .include_router (cloud_router )
8284
8385
8486@app .on_event ("startup" )
@@ -89,8 +91,10 @@ async def startup():
8991
9092@app .on_event ("shutdown" )
9193async def shutdown ():
92- """Clean up database connections on shutdown."""
94+ """Clean up database connections and HTTP clients on shutdown."""
9395 await close_db ()
96+ from server .nextcloud_client import close_all_clients
97+ await close_all_clients ()
9498
9599
96100# Configure CORS for browser access
@@ -1040,200 +1044,6 @@ async def get_element_properties(model_id: str, global_id: str):
10401044 return props
10411045
10421046
1043- # ==========================================================================
1044- # Cloud storage (Nextcloud) — opt-in via environment variables
1045- # ==========================================================================
1046-
1047- NEXTCLOUD_URL = os .environ .get ("NEXTCLOUD_URL" , "" )
1048- NEXTCLOUD_SERVICE_USER = os .environ .get ("NEXTCLOUD_SERVICE_USER" , "" )
1049- NEXTCLOUD_SERVICE_PASS = os .environ .get ("NEXTCLOUD_SERVICE_PASS" , "" )
1050- CLOUD_ENABLED = bool (NEXTCLOUD_URL and NEXTCLOUD_SERVICE_USER and NEXTCLOUD_SERVICE_PASS )
1051-
1052- _nextcloud_client = None
1053-
1054-
1055- def get_nextcloud_client ():
1056- """Get or create the singleton NextcloudClient.
1057-
1058- Returns the client if cloud storage is configured, otherwise raises 503.
1059- """
1060- global _nextcloud_client
1061- if not CLOUD_ENABLED :
1062- raise HTTPException (
1063- status_code = 503 ,
1064- detail = "Cloud storage is not configured" ,
1065- )
1066- if _nextcloud_client is None :
1067- from server .nextcloud_client import NextcloudClient
1068-
1069- _nextcloud_client = NextcloudClient (
1070- base_url = NEXTCLOUD_URL ,
1071- username = NEXTCLOUD_SERVICE_USER ,
1072- password = NEXTCLOUD_SERVICE_PASS ,
1073- )
1074- return _nextcloud_client
1075-
1076-
1077- @app .get ("/api/cloud/status" )
1078- async def cloud_status ():
1079- """Check if cloud storage is enabled and reachable."""
1080- from server .models .cloud import CloudStatusResponse
1081-
1082- if not CLOUD_ENABLED :
1083- return CloudStatusResponse (enabled = False , connected = False )
1084-
1085- client = get_nextcloud_client ()
1086- try :
1087- connected = await client .test_connection ()
1088- except Exception :
1089- connected = False
1090-
1091- return CloudStatusResponse (enabled = True , connected = connected )
1092-
1093-
1094- @app .get ("/api/cloud/projects" )
1095- async def cloud_list_projects ():
1096- """List available project folders in Nextcloud."""
1097- from server .models .cloud import CloudProjectItem , CloudProjectsResponse
1098-
1099- client = get_nextcloud_client ()
1100- try :
1101- items = await client .list_projects ()
1102- except Exception as exc :
1103- raise HTTPException (status_code = 502 , detail = str (exc )) from exc
1104-
1105- return CloudProjectsResponse (
1106- projects = [
1107- CloudProjectItem (name = item .name , last_modified = item .last_modified )
1108- for item in items
1109- ]
1110- )
1111-
1112-
1113- @app .get ("/api/cloud/projects/{project}/files" )
1114- async def cloud_list_files (project : str ):
1115- """List BCF files in a project's tool subdirectory."""
1116- from server .models .cloud import CloudFileItem , CloudFilesResponse
1117-
1118- client = get_nextcloud_client ()
1119- try :
1120- items = await client .list_files (project )
1121- except Exception as exc :
1122- from server .nextcloud_client import NextcloudError
1123-
1124- if isinstance (exc , NextcloudError ) and exc .status_code :
1125- raise HTTPException (
1126- status_code = exc .status_code , detail = str (exc )
1127- ) from exc
1128- raise HTTPException (status_code = 502 , detail = str (exc )) from exc
1129-
1130- return CloudFilesResponse (
1131- project = project ,
1132- files = [
1133- CloudFileItem (
1134- name = item .name ,
1135- size = item .content_length ,
1136- last_modified = item .last_modified ,
1137- )
1138- for item in items
1139- ],
1140- )
1141-
1142-
1143- @app .get ("/api/cloud/projects/{project}/files/{filename}" )
1144- async def cloud_download_file (project : str , filename : str ):
1145- """Download a file from a project's tool subdirectory."""
1146- client = get_nextcloud_client ()
1147- try :
1148- content = await client .download_file (project , filename )
1149- except Exception as exc :
1150- from server .nextcloud_client import NextcloudError
1151-
1152- if isinstance (exc , NextcloudError ) and exc .status_code :
1153- raise HTTPException (
1154- status_code = exc .status_code , detail = str (exc )
1155- ) from exc
1156- raise HTTPException (status_code = 502 , detail = str (exc )) from exc
1157-
1158- return Response (
1159- content = content ,
1160- media_type = "application/octet-stream" ,
1161- headers = {
1162- "Content-Disposition" : f'attachment; filename="{ filename } "' ,
1163- },
1164- )
1165-
1166-
1167- @app .put ("/api/cloud/projects/{project}/files/{filename}" )
1168- async def cloud_upload_file (
1169- project : str ,
1170- filename : str ,
1171- file : UploadFile = File (...),
1172- ):
1173- """Upload a file to a project's tool subdirectory."""
1174- from server .models .cloud import CloudUploadResponse
1175-
1176- client = get_nextcloud_client ()
1177- content = await file .read ()
1178- try :
1179- await client .upload_file (project , filename , content )
1180- except Exception as exc :
1181- from server .nextcloud_client import NextcloudError
1182-
1183- if isinstance (exc , NextcloudError ) and exc .status_code :
1184- raise HTTPException (
1185- status_code = exc .status_code , detail = str (exc )
1186- ) from exc
1187- raise HTTPException (status_code = 502 , detail = str (exc )) from exc
1188-
1189- return CloudUploadResponse (success = True , project = project , filename = filename )
1190-
1191-
1192- @app .delete ("/api/cloud/projects/{project}/files/{filename}" )
1193- async def cloud_delete_file (project : str , filename : str ):
1194- """Delete a file from a project's tool subdirectory."""
1195- from server .models .cloud import CloudDeleteResponse
1196-
1197- client = get_nextcloud_client ()
1198- try :
1199- await client .delete_file (project , filename )
1200- except Exception as exc :
1201- from server .nextcloud_client import NextcloudError
1202-
1203- if isinstance (exc , NextcloudError ) and exc .status_code :
1204- raise HTTPException (
1205- status_code = exc .status_code , detail = str (exc )
1206- ) from exc
1207- raise HTTPException (status_code = 502 , detail = str (exc )) from exc
1208-
1209- return CloudDeleteResponse (success = True , project = project , filename = filename )
1210-
1211-
1212- @app .post ("/api/cloud/projects/{project}/save" )
1213- async def cloud_save_bcf (
1214- project : str ,
1215- file : UploadFile = File (...),
1216- filename : str = Form ("validation.bcf" ),
1217- ):
1218- """Convenience endpoint: save a BCF file to a project's cloud folder."""
1219- from server .models .cloud import CloudUploadResponse
1220-
1221- client = get_nextcloud_client ()
1222- content = await file .read ()
1223- try :
1224- await client .upload_file (project , filename , content )
1225- except Exception as exc :
1226- from server .nextcloud_client import NextcloudError
1227-
1228- if isinstance (exc , NextcloudError ) and exc .status_code :
1229- raise HTTPException (
1230- status_code = exc .status_code , detail = str (exc )
1231- ) from exc
1232- raise HTTPException (status_code = 502 , detail = str (exc )) from exc
1233-
1234- return CloudUploadResponse (success = True , project = project , filename = filename )
1235-
1236-
12371047# ==========================================================================
12381048# Static file serving — serve built frontend in production
12391049# ==========================================================================
0 commit comments