11import logging
22import asyncio
3+ import json
34from datetime import datetime , timedelta
45from http import HTTPStatus
6+ from pathlib import Path
57from fastapi import Depends , HTTPException
8+ from fastapi .responses import FileResponse
69from sqlalchemy .exc import SQLAlchemyError
710from sqlalchemy .orm import Session
811from urllib .parse import urlparse
1518 RepositoryCreateRequest ,
1619 RepositoryUpdateRequest ,
1720 RepositoryResponse ,
18- RepositoriesListResponse
21+ RepositoriesListResponse ,
22+ RepositoryImageTokenResponse ,
1923)
20- from . import api_with_auth
24+ from app .config import config
25+ from app .utils .jwt_tokens import create_scoped_token_response , validate_scoped_token
26+ from . import api_with_auth , api_no_auth
2127
2228FRAMEOS_SAMPLES_URL = "https://repo.frameos.net/samples/repository.json"
2329FRAMEOS_GALLERY_URL = "https://repo.frameos.net/gallery/repository.json"
2430
31+ SYSTEM_REPOSITORIES_PATH = Path (__file__ ).resolve ().parents [3 ] / "repo" / "scenes"
32+
33+
34+ def _system_template_subject (repository_slug : str , template_slug : str ) -> str :
35+ return f"system-template={ repository_slug } /{ template_slug } "
36+
37+
38+ def _load_template_definition (repository_slug : str , template_dir : Path ):
39+ template_path = template_dir / "template.json"
40+ if not template_path .is_file ():
41+ return None
42+
43+ with template_path .open ("r" , encoding = "utf-8" ) as template_file :
44+ template_data = json .load (template_file )
45+
46+ image_path = template_data .get ("image" )
47+ if image_path :
48+ template_data ["image" ] = f"/api/repositories/system/{ repository_slug } /templates/{ template_dir .name } /image"
49+
50+ scenes_reference = template_data .get ("scenes" )
51+ if isinstance (scenes_reference , str ):
52+ scenes_path = _resolve_template_resource (template_dir , scenes_reference )
53+ if scenes_path and scenes_path .is_file ():
54+ with scenes_path .open ("r" , encoding = "utf-8" ) as scenes_file :
55+ template_data ["scenes" ] = json .load (scenes_file )
56+ else :
57+ template_data ["scenes" ] = []
58+
59+ return template_data
60+
61+
62+ def _resolve_template_resource (base_dir : Path , resource_path : str ) -> Path | None :
63+ if not resource_path :
64+ return None
65+
66+ relative_path = resource_path [2 :] if resource_path .startswith ("./" ) else resource_path
67+ candidate_path = (base_dir / relative_path ).resolve ()
68+
69+ try :
70+ candidate_path .relative_to (base_dir .resolve ())
71+ except ValueError :
72+ return None
73+
74+ return candidate_path
75+
76+
77+ def _load_system_repository (repository_dir : Path ):
78+ repository_slug = repository_dir .name
79+ repository_metadata_path = repository_dir / "repository.json"
80+ metadata : dict [str , str | None ] = {}
81+ if repository_metadata_path .is_file ():
82+ with repository_metadata_path .open ("r" , encoding = "utf-8" ) as repository_file :
83+ metadata = json .load (repository_file )
84+
85+ templates = []
86+ for template_dir in sorted (path for path in repository_dir .iterdir () if path .is_dir ()):
87+ template_definition = _load_template_definition (repository_slug , template_dir )
88+ if template_definition :
89+ templates .append (template_definition )
90+
91+ return {
92+ "id" : f"system-{ repository_slug } " ,
93+ "name" : metadata .get ("name" ) or repository_slug .title (),
94+ "description" : metadata .get ("description" ),
95+ "url" : f"/api/repositories/system/{ repository_slug } /repository.json" ,
96+ "last_updated_at" : None ,
97+ "templates" : templates ,
98+ }
99+
25100
26101@api_with_auth .post ("/repositories" , response_model = RepositoryResponse , status_code = 201 )
27102async def create_repository (data : RepositoryCreateRequest , db : Session = Depends (get_db )):
@@ -44,32 +119,102 @@ async def create_repository(data: RepositoryCreateRequest, db: Session = Depends
44119 logging .error (f'Database error: { e } ' )
45120 raise HTTPException (status_code = 500 , detail = "Database error" )
46121
122+ @api_with_auth .get ("/repositories/system" , response_model = RepositoriesListResponse )
123+ async def get_system_repositories (db : Session = Depends (get_db )):
124+ if not SYSTEM_REPOSITORIES_PATH .exists ():
125+ return []
126+
127+ repositories = []
128+ paths = [path for path in SYSTEM_REPOSITORIES_PATH .iterdir () if path .is_dir ()]
129+ for repository_dir in paths :
130+ repositories .append (_load_system_repository (repository_dir ))
131+
132+ # Sort order: samples first, then gallery, then everything else alphabetically
133+ def sort_key (repo ):
134+ if repo .get ('id' ) == "system-samples" :
135+ return (0 , "" )
136+ elif repo .get ('id' ) == "system-gallery" :
137+ return (1 , "" )
138+ return (2 , repo .get ('id' ) or "" )
139+
140+ repositories .sort (key = sort_key )
141+
142+ return repositories
143+
144+
145+ @api_with_auth .get (
146+ "/repositories/system/{repository_slug}/templates/{template_slug}/image_token" ,
147+ response_model = RepositoryImageTokenResponse ,
148+ )
149+ async def get_system_repository_image_token (repository_slug : str , template_slug : str ):
150+ return create_scoped_token_response (
151+ _system_template_subject (repository_slug , template_slug )
152+ )
153+
154+
155+ @api_no_auth .get ("/repositories/system/{repository_slug}/templates/{template_slug}/image" )
156+ async def get_system_repository_image (repository_slug : str , template_slug : str , token : str ):
157+ if config .HASSIO_RUN_MODE != 'ingress' :
158+ validate_scoped_token (
159+ token ,
160+ expected_subject = _system_template_subject (repository_slug , template_slug ),
161+ )
162+
163+ repository_path = SYSTEM_REPOSITORIES_PATH / repository_slug
164+ if not repository_path .is_dir ():
165+ raise HTTPException (status_code = 404 , detail = "Repository not found" )
166+
167+ template_path = repository_path / template_slug
168+ if not template_path .is_dir ():
169+ raise HTTPException (status_code = 404 , detail = "Template not found" )
170+
171+ template_definition_path = template_path / "template.json"
172+ if not template_definition_path .is_file ():
173+ raise HTTPException (status_code = 404 , detail = "Template not found" )
174+
175+ with template_definition_path .open ("r" , encoding = "utf-8" ) as template_file :
176+ template_data = json .load (template_file )
177+
178+ image_reference = template_data .get ("image" )
179+ if not isinstance (image_reference , str ) or not image_reference :
180+ raise HTTPException (status_code = 404 , detail = "Template image not found" )
181+
182+ image_path = _resolve_template_resource (template_path , image_reference )
183+ if not image_path or not image_path .is_file ():
184+ raise HTTPException (status_code = 404 , detail = "Template image not found" )
185+
186+ return FileResponse (image_path )
187+
47188@api_with_auth .get ("/repositories" , response_model = RepositoriesListResponse )
48189async def get_repositories (db : Session = Depends (get_db )):
49190 try :
50- # Remove old repo if it exists
51- if db .query (Settings ).filter_by (key = "@system/repository_init_done" ).first ():
52- old_url = "https://repo.frameos.net/versions/0/templates.json"
53- repository = db .query (Repository ).filter_by (url = old_url ).first ()
54- if repository :
55- db .delete (repository )
56- db .delete (db .query (Settings ).filter_by (key = "@system/repository_init_done" ).first ())
57- db .commit ()
191+ if db .query (Settings ).filter_by (key = "@system/repository_global_cleanup" ).first ():
192+ # We're good here. No need to do all the checks
193+ pass
194+ else :
195+ # Remove old repo if it exists
196+ if db .query (Settings ).filter_by (key = "@system/repository_init_done" ).first ():
197+ old_url = "https://repo.frameos.net/versions/0/templates.json"
198+ repository = db .query (Repository ).filter_by (url = old_url ).first ()
199+ if repository :
200+ db .delete (repository )
201+ db .delete (db .query (Settings ).filter_by (key = "@system/repository_init_done" ).first ())
202+ db .commit ()
58203
59- # Create samples repo if not done
60- if not db .query (Settings ).filter_by (key = "@system/repository_samples_done" ).first ():
61- repository = Repository (name = "" , url = FRAMEOS_SAMPLES_URL )
62- await repository .update_templates ()
63- db .add (repository )
64- db .add (Settings (key = "@system/repository_samples_done" , value = "true" ))
65- db .commit ()
204+ # delete old gallery/samples repos
205+ if db .query (Settings ).filter_by (key = "@system/repository_samples_done" ).first ():
206+ repository = db .query (Repository ).filter_by (url = FRAMEOS_SAMPLES_URL ).first ()
207+ if repository :
208+ db .delete (repository )
209+ db .delete (db .query (Settings ).filter_by (key = "@system/repository_samples_done" ).first ())
210+
211+ if db .query (Settings ).filter_by (key = "@system/repository_gallery_done" ).first ():
212+ repository = db .query (Repository ).filter_by (url = FRAMEOS_GALLERY_URL ).first ()
213+ if repository :
214+ db .delete (repository )
215+ db .delete (db .query (Settings ).filter_by (key = "@system/repository_gallery_done" ).first ())
66216
67- # Create gallery repo if not done
68- if not db .query (Settings ).filter_by (key = "@system/repository_gallery_done" ).first ():
69- repository = Repository (name = "" , url = FRAMEOS_GALLERY_URL )
70- await repository .update_templates ()
71- db .add (repository )
72- db .add (Settings (key = "@system/repository_gallery_done" , value = "true" ))
217+ db .add (Settings (key = "@system/repository_global_cleanup" , value = "true" ))
73218 db .commit ()
74219
75220 repositories = db .query (Repository ).all ()
0 commit comments