Skip to content

Commit 46f74e4

Browse files
authored
Merge pull request #3 from linuxserver/add-fields
Add fields
2 parents 91516dd + a3cc30f commit 46f74e4

File tree

8 files changed

+411
-61
lines changed

8 files changed

+411
-61
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ docker run -d \
3535
-e DB_FILE=/config/api.db `#optional` \
3636
-e INVALIDATE_HOURS=24 `#optional` \
3737
-e PAT=token `#optional` \
38+
-e URL=http://localhost:8000 `#optional` \
3839
-p 8000:8000 \
3940
-v /path/to/lsio-api/config:/config \
4041
--restart unless-stopped \

readme-vars.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ full_custom_readme: |
4040
-e DB_FILE=/config/api.db `#optional` \
4141
-e INVALIDATE_HOURS=24 `#optional` \
4242
-e PAT=token `#optional` \
43+
-e URL=http://localhost:8000 `#optional` \
4344
-p 8000:8000 \
4445
-v /path/to/lsio-api/config:/config \
4546
--restart unless-stopped \

root/app/api.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,49 @@
1-
from fastapi import FastAPI
1+
from fastapi import FastAPI, HTTPException
2+
from fastapi.openapi.docs import get_swagger_ui_html
3+
from fastapi.responses import JSONResponse
4+
from fastapi.staticfiles import StaticFiles
25
from keyvaluestore import KeyValueStore
3-
from models import ImagesResponse, ImagesData
6+
from models import ImagesResponse
7+
from pydantic import ValidationError
8+
import json
9+
import os
10+
import traceback
411

5-
api = FastAPI(docs_url="/")
12+
URL = os.environ.get("URL", "http://localhost:8000")
13+
api = FastAPI(docs_url=None, redoc_url=None, version="1.0", title="LinuxServer API", servers=[{"url": URL}])
14+
api.mount("/static", StaticFiles(directory="static"), name="static")
15+
16+
17+
@api.get("/", include_in_schema=False)
18+
async def swagger_ui_html():
19+
return get_swagger_ui_html(openapi_url="/openapi.json", title="LinuxServer API", swagger_favicon_url="/static/logo.png")
620

721
@api.get("/health", summary="Get the health status")
822
async def health():
923
return "Success"
1024

11-
@api.get("/api/v1/images", response_model=ImagesResponse, summary="Get a list of images")
12-
async def images():
25+
async def get_images():
1326
with KeyValueStore() as kv:
14-
return ImagesResponse(status="OK", data=ImagesData.model_validate_json(kv["images"]))
27+
return kv["images"]
28+
29+
@api.get("/api/v1/images", response_model=ImagesResponse, summary="Get a list of images", response_model_exclude_none=True)
30+
async def images(include_config: bool = False, include_deprecated: bool = False):
31+
try:
32+
response = await get_images()
33+
image_response = ImagesResponse.model_validate_json(response)
34+
if not include_deprecated:
35+
image_response.exclude_deprecated()
36+
if not include_config:
37+
image_response.exclude_config()
38+
return image_response
39+
except ValidationError:
40+
print(traceback.format_exc())
41+
response = await get_images()
42+
content = json.loads(response)
43+
return JSONResponse(content=content)
44+
except Exception:
45+
print(traceback.format_exc())
46+
raise HTTPException(status_code=404, detail="Not found")
1547

1648
if __name__ == "__main__":
1749
api.run()

root/app/keyvaluestore.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,27 @@
22
import sqlite3
33

44
DB_FILE = os.environ.get("DB_FILE", "/config/api.db")
5+
# Increment to drop tables and start over
6+
DB_SCHEMA_VERSION = 1
57

68

9+
def set_db_schema():
10+
conn = sqlite3.connect(DB_FILE)
11+
conn.execute("CREATE TABLE IF NOT EXISTS db_schema (key TEXT UNIQUE, version INTEGER DEFAULT 0)")
12+
is_updated = conn.execute(f"SELECT 1 FROM db_schema WHERE version = {DB_SCHEMA_VERSION}").fetchone() is not None
13+
if not is_updated:
14+
conn.execute(f"DROP TABLE IF EXISTS kv")
15+
conn.execute(f"REPLACE INTO db_schema (key, version) VALUES('schema_version', {DB_SCHEMA_VERSION})")
16+
conn.commit()
17+
conn.close()
18+
719
class KeyValueStore(dict):
820
def __init__( self, invalidate_hours=24, readonly=True):
921
self.invalidate_hours = invalidate_hours
1022
self.readonly = readonly
1123
if not readonly:
1224
self.conn = sqlite3.connect(DB_FILE)
13-
self.conn.execute("CREATE TABLE IF NOT EXISTS kv (key TEXT UNIQUE, value TEXT, updated_at TEXT)")
25+
self.conn.execute("CREATE TABLE IF NOT EXISTS kv (key TEXT UNIQUE, value TEXT, updated_at TEXT, schema_version INTEGER)")
1426
self.conn.commit()
1527
self.conn.close()
1628
def __enter__(self):
@@ -21,10 +33,13 @@ def __exit__(self, exc_type, exc_val, exc_tb):
2133
self.conn.commit()
2234
self.conn.close()
2335
def __contains__(self, key):
24-
return self.conn.execute(f"SELECT 1 FROM kv WHERE key = '{key}' AND updated_at >= DATETIME('now', '-{self.invalidate_hours} hours')").fetchone() is not None
36+
where_clause = "" if self.invalidate_hours == 0 else f" AND updated_at >= DATETIME('now', '-{self.invalidate_hours} hours', 'utc')"
37+
return self.conn.execute(f"SELECT 1 FROM kv WHERE key = '{key}' {where_clause}").fetchone() is not None
2538
def __getitem__(self, key):
2639
item = self.conn.execute("SELECT value FROM kv WHERE key = ?", (key,)).fetchone()
2740
return item[0] if item else None
28-
def __setitem__(self, key, value):
29-
self.conn.execute("REPLACE INTO kv (key, value, updated_at) VALUES (?,?, CURRENT_TIMESTAMP)", (key, value))
41+
def set_value(self, key, value, schema_version):
42+
self.conn.execute("REPLACE INTO kv (key, value, updated_at, schema_version) VALUES (?, ?, DATETIME('now', 'utc'), ?)", (key, value, schema_version))
3043
self.conn.commit()
44+
def is_current_schema(self, key, schema_version):
45+
return self.conn.execute(f"SELECT 1 FROM kv WHERE key = '{key}' AND schema_version = {schema_version}").fetchone() is not None

root/app/lsio_github.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from github import Auth
2+
from github import Github
3+
4+
import os
5+
import yaml
6+
7+
PAT = os.environ.get("PAT", None)
8+
GH_AUTH = Auth.Token(PAT) if PAT else None
9+
GH = Github(auth=GH_AUTH)
10+
11+
12+
def get_repos():
13+
org = GH.get_organization("linuxserver")
14+
return org.get_repos()
15+
16+
def get_file(repo, branch, path, is_yaml=False):
17+
try:
18+
content = repo.get_contents(path, ref=branch).decoded_content
19+
return yaml.load(content, Loader=yaml.CLoader) if is_yaml else content
20+
except:
21+
return None
22+
23+
def get_last_stable_release(repo):
24+
for release in repo.get_releases():
25+
if release.prerelease:
26+
continue
27+
return release.tag_name, str(release.published_at)
28+
return "latest", str(repo.pushed_at)
29+
30+
def get_readme_vars(repo):
31+
return (get_file(repo, "master", "readme-vars.yml", is_yaml=True) or
32+
get_file(repo, "main", "readme-vars.yml", is_yaml=True) or
33+
get_file(repo, "develop", "readme-vars.yml", is_yaml=True) or
34+
get_file(repo, "nightly", "readme-vars.yml", is_yaml=True))
35+
36+
def print_rate_limit():
37+
ratelimit = GH.get_rate_limit().core
38+
print(f"Github ratelimit - {ratelimit.remaining}/{ratelimit.limit} resets at {ratelimit.reset}")

root/app/models.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,111 @@
11
from pydantic import BaseModel
22

3+
# Increment when updating schema
4+
IMAGES_SCHEMA_VERSION = 1
5+
6+
7+
class Tag(BaseModel):
8+
tag: str
9+
desc: str
10+
11+
class Architecture(BaseModel):
12+
arch: str
13+
tag: str
14+
15+
class Changelog(BaseModel):
16+
date: str
17+
desc: str
18+
19+
class Volume(BaseModel):
20+
path: str
21+
host_path: str
22+
desc: str
23+
optional: bool
24+
25+
class Port(BaseModel):
26+
external: str
27+
internal: str
28+
desc: str
29+
optional: bool
30+
31+
class EnvVar(BaseModel):
32+
name: str
33+
value: str
34+
desc: str
35+
optional: bool
36+
37+
class EnvVar(BaseModel):
38+
name: str
39+
value: str
40+
desc: str
41+
optional: bool
42+
43+
class Custom(BaseModel):
44+
name: str
45+
name_compose: str
46+
value: str | list[str]
47+
desc: str
48+
optional: bool
49+
50+
class SecurityOpt(BaseModel):
51+
run_var: str
52+
compose_var: str
53+
desc: str
54+
optional: bool
55+
56+
class Device(BaseModel):
57+
path: str
58+
host_path: str
59+
desc: str
60+
optional: bool
61+
62+
class Cap(BaseModel):
63+
cap_add: str
64+
desc: str
65+
optional: bool
66+
67+
class Hostname(BaseModel):
68+
hostname: str
69+
desc: str
70+
optional: bool
71+
72+
class MacAddress(BaseModel):
73+
mac_address: str
74+
desc: str
75+
optional: bool
76+
77+
class Config(BaseModel):
78+
application_setup: str | None = None
79+
readonly_supported: bool | None = None
80+
nonroot_supported: bool | None = None
81+
privileged: bool | None = None
82+
networking: str | None = None
83+
hostname: Hostname | None = None
84+
mac_address: MacAddress | None = None
85+
env_vars: list[EnvVar] | None = None
86+
volumes: list[Volume] | None = None
87+
ports: list[Port] | None = None
88+
custom: list[Custom] | None = None
89+
security_opt: list[SecurityOpt] | None = None
90+
devices: list[Device] | None = None
91+
caps: list[Cap] | None = None
392

493
class Image(BaseModel):
594
name: str
95+
github_url: str
96+
project_url: str | None = None
97+
project_logo: str | None = None
98+
description: str
699
version: str
100+
version_timestamp: str
7101
category: str
8102
stable: bool
9103
deprecated: bool
104+
stars: int
105+
tags: list[Tag]
106+
architectures: list[Architecture]
107+
changelog: list[Changelog] | None = None
108+
config: Config | None = None
10109

11110
class Repository(BaseModel):
12111
linuxserver: list[Image]
@@ -16,4 +115,13 @@ class ImagesData(BaseModel):
16115

17116
class ImagesResponse(BaseModel):
18117
status: str
118+
last_updated: str
19119
data: ImagesData
120+
121+
def exclude_config(self):
122+
for image in self.data.repositories.linuxserver:
123+
image.config = None
124+
125+
def exclude_deprecated(self):
126+
images = self.data.repositories.linuxserver
127+
self.data.repositories.linuxserver = list(filter(lambda image: not image.deprecated, images))

root/app/static/logo.png

19.3 KB
Loading

0 commit comments

Comments
 (0)