Atreides Services 0.0.4: image 0.0.2 en het slot Backups

- image 0.0.2 met digest, via tools/release-bump.mjs; de digest komt
  overeen met wat het register anoniem onder die tag heeft.
- folderAccess-slot backups (Backups) op server:/data/backups, met als
  standaard ${APP_DATA_DIR}/data/app/backups en een .gitkeep. Zonder
  gekozen map verandert er niets: dezelfde plek als zonder het slot.
- test_appstore_vorm.py toetst elk folderAccess-slot: geldig en uniek id,
  een naam, een service die bestaat en een standaardmount op hetzelfde
  doelpad. Mutatie op id, doelpad en service valt elk op de juiste toets.

Het slot Price history volgt met fase 2c van het plan Netwerkopslag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HarmenandClaude Opus 5.5 committed 2026-10-09 16:16:33 +02:00
1 parent e86208ddfe
commit 519002902c
4 files changed
+172 -5

No files matched your search

+141
View File
@@ -388,6 +388,146 @@ def test_verplaatsbare_opslag(u, app):
not fout, "geweigerd: %r" % fout)
def folder_slots(tekst):
"""De slots onder `folderAccess:` als lijst van dicts, elk met een lijst `mounts`.
Geen YAML-lezer, zie veld(). Wat telt is de inspringing: een streepje op de
diepte van het eerste streepje begint een slot, een dieper streepje een mount
in dat slot. Een sleutel zonder streepje hoort bij de mount zolang hij dieper
staat dan de sleutels van het slot.
"""
regels = tekst.splitlines()
start = next((i for i, r in enumerate(regels) if r.startswith("folderAccess:")), None)
if start is None:
return None
slots = []
slot_streep = None
huidig = doel = None
for r in regels[start + 1:]:
if r and not r[0].isspace():
break
kaal = r.strip()
if not kaal or kaal.startswith("#"):
continue
inspring = len(r) - len(r.lstrip())
if kaal.startswith("- "):
kaal = kaal[2:].strip()
if slot_streep is None or inspring <= slot_streep:
slot_streep = inspring
huidig = doel = {"mounts": []}
slots.append(huidig)
elif huidig is not None:
doel = {}
huidig["mounts"].append(doel)
elif huidig is not None and doel is not huidig and inspring <= slot_streep + 2:
doel = huidig
if doel is None or ":" not in kaal:
continue
sleutel, waarde = kaal.split(":", 1)
waarde = waarde.strip().strip('"').strip("'")
if waarde:
doel[sleutel.strip()] = waarde
return slots
def doelpaden_per_service(tekst):
"""Per service in de compose de doelpaden van de korte `volumes:`-regels."""
uit = {}
in_services = False
service = None
in_volumes = False
volumes_inspring = 0
for regel in tekst.splitlines():
if regel.startswith("services:"):
in_services = True
continue
if not in_services:
continue
if regel and not regel[0].isspace():
break
kaal = regel.strip()
if not kaal or kaal.startswith("#"):
continue
inspring = len(regel) - len(regel.lstrip())
if inspring == 2 and kaal.endswith(":"):
service = kaal[:-1]
uit[service] = []
in_volumes = False
continue
if service is None:
continue
if kaal == "volumes:":
in_volumes = True
volumes_inspring = inspring
continue
if in_volumes:
if kaal.startswith("- "):
delen = kaal[2:].strip().strip('"').split(":")
if len(delen) >= 2:
uit[service].append(delen[1])
elif inspring <= volumes_inspring:
in_volumes = False
return uit
def test_folder_access(u, app):
"""Elk `folderAccess`-slot wijst naar een service die bestaat, met een standaardmount.
umbrelOS 2.0 bewaart de keuze van de gebruiker per `id`, en zoekt de standaard
van een slot in de compose: een mount op dezelfde `service` en hetzelfde
`targetPath`. Klopt een van de twee niet, dan meldt niemand iets: het slot is
dan "Optional" met een lege standaard, en tot de gebruiker een map kiest
schrijft de app in wat er toevallig onder dat pad ligt, in het slechtste geval
zijn eigen containerlaag, die bij de volgende update weg is. In deze store heeft elk slot
daarom een standaardmount onder data/; dat die map in de repo staat, toetst
test_data_onder_data.
`umbrel-downloads` is gereserveerd voor de gedeelde Downloads-map. Zie
Umbrel-appstore-spec.md §7.
"""
slots = folder_slots(lees(os.path.join(REPO, app, "umbrel-app.yml")))
if slots is None:
return
u.check("%s: folderAccess heeft minstens een slot" % app, bool(slots))
ids = [s.get("id") for s in slots]
fout_id = [i for i in ids if not i or i != i.lower() or " " in i or "_" in i
or i == "umbrel-downloads"]
u.check("%s: elk slot-id is lowercase kebab-case en niet gereserveerd" % app,
not fout_id, "ids: %r" % ids)
u.check("%s: de slot-ids zijn uniek" % app,
len(set(ids)) == len(ids), "ids: %r" % ids)
zonder_naam = [s.get("id") for s in slots if not s.get("name")]
u.check("%s: elk slot heeft een naam" % app,
not zonder_naam, "zonder naam: %r" % zonder_naam)
pad = os.path.join(REPO, app, "docker-compose.yml")
if not os.path.isfile(pad):
return
doelen = doelpaden_per_service(lees(pad))
zonder_mount = [s.get("id") for s in slots if not s["mounts"]]
u.check("%s: elk slot heeft minstens een mount" % app,
not zonder_mount, "zonder mount: %r" % zonder_mount)
onbekend = []
zonder_standaard = []
for s in slots:
for m in s["mounts"]:
service = m.get("service")
doel = m.get("targetPath", "")
if service not in doelen:
onbekend.append("%s: %r" % (s.get("id"), service))
elif doel not in doelen[service]:
zonder_standaard.append("%s: %s op %s" % (s.get("id"), doel, service))
u.check("%s: elke slot-mount wijst naar een service die bestaat" % app,
not onbekend, "onbekend: %r, aanwezig: %r" % (onbekend, sorted(doelen)))
u.check("%s: elke slot-mount heeft een standaardmount in de compose" % app,
not zonder_standaard, "ontbreekt: %r" % zonder_standaard)
def test_containernamen_zijn_volledig(u, app):
"""Verwijzingen tussen containers gebruiken de volledige naam.
@@ -596,6 +736,7 @@ def main():
test_image_tag_is_de_gebouwde(u, app)
test_data_onder_data(u, app)
test_verplaatsbare_opslag(u, app)
test_folder_access(u, app)
test_containernamen_zijn_volledig(u, app)
test_geen_werkbestanden_in_de_app_map(u, app)
test_templates_hebben_geen_losse_dollars(u, app)
Whitespace-only changes.
@@ -34,7 +34,7 @@ services:
# DE TAG IS DE VERSIE VAN DE IMAGE EN NIET DIE VAN DE APP, net als bij Atreides
# Assets: `version` in umbrel-app.yml moet bij élke wijziging omhoog, ook als
# alleen dit bestand verandert.
image: sc.kamenier-hamer.nl/sysop/atreides-services:0.0.1@sha256:57e509700bed543048544eb6d1fece9386c2e0a715dfb901e381d4f9247b2815
image: sc.kamenier-hamer.nl/sysop/atreides-services:0.0.2@sha256:f46330027f8289ba263dcbca15bda5ea00de8c2f64bcb17b71974ab7497c60ad
restart: on-failure
# Geen `user:` nodig: de image zet zelf `USER node` (uid 1000). umbrelOS
# maakt de datamap aan als `umbrel`, ook uid 1000, dus node mag erin
@@ -64,3 +64,10 @@ services:
# (.gitkeep), en mag hier geen mount-optie bij die umbreld niet kent.
# Staat de map op een USB-schijf, dan gaat hij NIET mee in de back-up.
- ${APP_DATA_DIR}/data/app:/data
# De standaard van het folderAccess-slot "backups" (umbrel-app.yml): zelfde
# service en zelfde doelpad, en precies de map die de app zonder deze regel
# ook zou gebruiken. Wijst de gebruiker het slot naar een NAS-map, dan
# vervangt umbreld alleen deze mount; de rest van /data blijft lokaal.
# Gemeten in het plan Netwerkopslag: een mount binnen /data verbergt de rest
# niet.
- ${APP_DATA_DIR}/data/app/backups:/data/backups
+23 -4
View File
@@ -14,9 +14,27 @@ id: whatsnext-atreides-services
# start dan niet meer. Zie Umbrel-appstore-spec.md §7.
storage:
dataRoot: data
# umbrelOS 2.0: een map die de gebruiker in de app-instellingen naar zijn
# Home-map, een USB-schijf of een netwerkshare kan wijzen. Er wordt niets
# gekopieerd; umbreld vervangt alleen de mount. De standaard is de mount op
# dezelfde service en hetzelfde doelpad in docker-compose.yml, en die wijst naar
# dezelfde plek als zonder dit slot, dus voor een bestaande installatie
# verandert er niets. De app schrijft in een eigen submap backups/ met een
# merkbestand, zodat dit slot en het latere "Price history" naar dezelfde
# NAS-map mogen wijzen.
#
# Het `id` is na de eerste release vast: umbrelOS bewaart de keuze van de
# gebruiker per id, dus hernoemen gooit die weg. Op een netwerkshare valt de map
# buiten de back-up van umbrelOS, en dat is hier juist de bedoeling.
folderAccess:
- id: backups
name: Backups
mounts:
- service: server
targetPath: /data/backups
category: finance
name: Atreides Services
version: "0.0.3"
version: "0.0.4"
tagline: Your own price feed, recorded around the clock
description: >-
A price service that runs on your Umbrel and keeps what it records. It fetches prices day
@@ -34,9 +52,10 @@ description: >-
mempool.space, and brings the administration app with its System page and an optional
login of its own. The API for other apps follows in a later version.
releaseNotes: >-
On umbrelOS 2.0 the app's storage can now be moved to a USB drive (ext4) in the app
settings. Keep in mind that umbrelOS does not back up an app whose storage is on a USB
drive.
The app now makes a checked backup every night, and keeps each closed month once. On
umbrelOS 2.0 you can point the new Backups folder at a folder on your NAS in the app
settings, so the backups live on another disk. Without that, they stay in the app's own
storage. The app writes them in a subfolder backups/ of the folder you pick.
developer: "WhatsNext?"
website: https://sc.kamenier-hamer.nl/sysop/UmbrelApps
dependencies: []