Eigener Datenordner (ablage.py)

Als EXE lagen Journal, Berichte und Log bisher direkt neben der EXE. Damit
laesst sich das Programm nicht in einen Ordner legen, in den Windows nicht
schreiben laesst - und beim Sichern uebersieht man leicht die Haelfte.

Jetzt: beim ersten Start ein Ordner "Daten" neben der EXE, alles darin. Geht
das nicht, weicht es nach LOCALAPPDATA aus. Was schon neben der EXE lag, zieht
mit um - sonst waere das Journal ploetzlich leer.

Ist der gemerkte Ordner nicht erreichbar (Stick ab, NAS aus), wird NICHTS
geaendert und der Merkzettel bleibt stehen: morgen steckt der Stick wieder
dran und alles ist da, wo es war.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKvGkdNW1vKdnMPAM9Bp3X
This commit is contained in:
TheMockTv 2026-09-02 21:44:51 +02:00
parent 65eec9937a
commit de71fce64f
2 changed files with 126 additions and 1 deletions

123
ablage.py Normal file
View file

@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
"""Wo das Programm seine Daten ablegt.
Als EXE legt sich das Programm beim ersten Start einen Ordner an - dort liegen
Journal, Berichte und Log beieinander. Zum Sichern reicht damit dieser eine
Ordner.
1. neben der EXE ein Unterordner "Daten"
2. geht das nicht (EXE liegt z. B. unter "Programme"), dann der Ordner
"Beherbergungssteuer" unter LOCALAPPDATA
Was schon da ist, wird benutzt; was fehlt, wird angelegt. Lagen die Dateien
noch direkt neben der EXE - so war es frueher -, ziehen sie mit um. Sonst waere
das Journal ploetzlich "leer" und die Meldung ans Amt faengt bei null an.
Aus der Quelle heraus (python app.py) bleibt alles, wo es immer war.
"""
import logging
import os
import sys
log = logging.getLogger("bst.ablage")
if getattr(sys, "frozen", False):
EXE_DIR = os.path.dirname(os.path.abspath(sys.argv[0]))
else:
EXE_DIR = os.path.dirname(os.path.abspath(__file__))
# Diese Dateien und Ordner gehoeren dem Programm und ziehen mit um.
EIGENE_DATEN = ("journal.sqlite3", "beherbergungssteuer.log", "berichte")
def _beschreibbar(ordner):
"""Kann in diesen Ordner geschrieben werden? Wird ausprobiert, nicht geraten."""
try:
os.makedirs(ordner, exist_ok=True)
probe = os.path.join(ordner, ".schreibprobe")
with open(probe, "w") as f:
f.write("x")
os.remove(probe)
return True
except OSError:
return False
def _zeiger_datei():
"""Merkzettel, WO der Datenordner liegt - liegt bewusst ausserhalb davon."""
heim = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
return os.path.join(heim, "Beherbergungssteuer", "datenordner.txt")
def _gemerkter_ordner():
"""(Pfad, Lage): Lage ist "ok", "unerreichbar" oder None (kein Merkzettel).
Faengt bewusst mehr als OSError ab: ein zerrissener Merkzettel (Stromausfall
beim Schreiben) enthaelt Null-Bytes oder kaputte Zeichen - der Fehler wuerde
sonst beim Import fliegen, also ohne Fenster.
"""
try:
with open(_zeiger_datei(), "r", encoding="utf-8") as f:
pfad = f.read().strip()
except (OSError, ValueError, UnicodeDecodeError) as e:
log.debug("Merkzettel nicht lesbar: %s", e)
return None, None
if not pfad or "\x00" in pfad or not os.path.isabs(pfad):
log.warning("Merkzettel unbrauchbar - es wird neu angelegt")
return None, None
try:
laufwerk = os.path.splitdrive(pfad)[0]
erreichbar = (not laufwerk) or os.path.exists(
laufwerk if laufwerk.startswith("\\\\") else laufwerk + os.sep)
except (OSError, ValueError):
erreichbar = False
if not erreichbar:
return pfad, "unerreichbar"
return (pfad, "ok") if _beschreibbar(pfad) else (pfad, "unerreichbar")
def _ordner_merken(pfad):
"""Erst daneben schreiben, dann umlegen - so ist der Merkzettel nie halb."""
ziel = _zeiger_datei()
try:
os.makedirs(os.path.dirname(ziel), exist_ok=True)
neben = ziel + ".neu"
with open(neben, "w", encoding="utf-8") as f:
f.write(pfad)
f.flush()
os.fsync(f.fileno())
os.replace(neben, ziel)
except (OSError, ValueError) as e:
log.warning("Datenordner konnte nicht gemerkt werden: %s", e)
def daten_ordner():
"""Der Ordner, in dem Journal, Berichte und Log liegen."""
if not getattr(sys, "frozen", False):
return EXE_DIR
gemerkt, lage = _gemerkter_ordner()
if lage == "ok":
return gemerkt
if lage == "unerreichbar":
# Stick ab, NAS aus: NICHT still woanders neu anfangen - sonst hat der
# Nutzer zwei Journale und keinen Weg zurueck.
log.warning("Datenordner %s ist nicht erreichbar - es wird nichts geändert", gemerkt)
return gemerkt if _beschreibbar(gemerkt) else EXE_DIR
kandidat = os.path.join(EXE_DIR, "Daten")
if not _beschreibbar(kandidat):
kandidat = os.path.join(os.environ.get("LOCALAPPDATA", EXE_DIR), "Beherbergungssteuer")
if not _beschreibbar(kandidat):
return EXE_DIR
_ordner_merken(kandidat)
for name in EIGENE_DATEN:
alt, neu = os.path.join(EXE_DIR, name), os.path.join(kandidat, name)
if os.path.exists(alt) and not os.path.exists(neu):
try:
os.replace(alt, neu)
log.info("%s in den Datenordner übernommen", name)
except OSError as e:
log.warning("%s ließ sich nicht übernehmen: %s", name, e)
return kandidat

4
app.py
View file

@ -21,11 +21,13 @@ import db
import xlsx_io
import firma as firma_lib
import bericht_pdf
import ablage
from modell import MONATE, STANDARD_SATZ, summiere, eur
from gui_monat import MonatsTab, SummenLeiste
from gui_dialoge import BuchungDialog
BASE_DIR = os.path.dirname(os.path.abspath(sys.argv[0] if getattr(sys, "frozen", False) else __file__))
# Als EXE liegt alles in einem eigenen Datenordner - siehe ablage.py.
BASE_DIR = ablage.daten_ordner()
JOURNAL = os.path.join(BASE_DIR, "journal.sqlite3")
LOGDATEI = os.path.join(BASE_DIR, "beherbergungssteuer.log")
BERICHTE = os.path.join(BASE_DIR, "berichte")