Backupfunktion erweitert:

- Versionsinfo im Archiv
	- Verzeichnis anpassbar
	- API-Call zum Erstellen von Verzeichnissen
Typos in definitions.py angepasst
write_admin_setting Funktion hinzugefügt
This commit is contained in:
Alexander Malzkuhn 2025-05-27 10:06:25 +02:00
parent 681ad9d3fe
commit e59e558a6e
9 changed files with 158 additions and 176 deletions

View File

@ -611,26 +611,21 @@ Dies kann nicht rückgängig gemacht werden!''')
ui.markdown("**Administrationsbenutzer:**") ui.markdown("**Administrationsbenutzer:**")
with ui.grid(columns=2): with ui.grid(columns=2):
def save_admin_settings(): def save_admin_settings():
output_dict = { } write_adminsetting("admin_user", admin_user.value)
output_dict["admin_user"] = admin_user.value
if admin_password.value != "": if admin_password.value != "":
output_dict["admin_password"] = hash_password(admin_password.value) write_adminsetting("admin_password", hash_password(admin_password.value))
else: else:
output_dict["admin_password"] = data["admin_password"] write_adminsetting("admin_password", data["admin_password"])
output_dict["port"] = port.value write_adminsetting("port", port.value)
output_dict["secret"] = secret write_adminsetting("secret", secret)
output_dict["touchscreen"] = touchscreen_switch.value write_adminsetting("touchscreen", touchscreen_switch.value)
output_dict["times_on_touchscreen"] = timestamp_switch.value write_adminsetting("times_on_touchscreen", timestamp_switch.value)
output_dict["photos_on_touchscreen"] = photo_switch.value write_adminsetting("photos_on_touchscreen", photo_switch.value)
output_dict["picture_height"] = picture_height_input.value write_adminsetting("picture_height", picture_height_input.value)
output_dict["button_height"] = button_height_input.value write_adminsetting("button_height", button_height_input.value)
output_dict["user_notes"] = notes_switch.value write_adminsetting("user_notes", notes_switch.value)
output_dict["holidays"] = data["holidays"] write_adminsetting("holidays", data["holidays"])
output_dict["version"] = app_version
json_dict = json.dumps(output_dict, indent=4)
with open(os.path.join(scriptpath, usersettingsfilename), "w") as outputfile:
outputfile.write(json_dict)
if int(old_port) != int(port.value): if int(old_port) != int(port.value):
with ui.dialog() as dialog, ui.card(): with ui.dialog() as dialog, ui.card():
ui.markdown("Damit die Porteinstellungen wirksam werden, muss der Server neu gestartet werden.") ui.markdown("Damit die Porteinstellungen wirksam werden, muss der Server neu gestartet werden.")
@ -1247,10 +1242,49 @@ Dies kann nicht rückgängig gemacht werden!''')
user_selection_changed() user_selection_changed()
with ui.tab_panel(backups): with ui.tab_panel(backups):
ui.markdown('**Backups**') try:
backupfolder = load_adminsettings()["backup_folder"]
except KeyError:
pass
try:
api_key = load_adminsettings()["backup_api_key"]
except:
api_key = ""
ui.label("Backupeinstellungen").classes('font-bold')
with ui.grid(columns='auto auto auto'):
ui.markdown("Backupordner:")
backupfolder_input = ui.input(value=backupfolder).props(f"size={len(backupfolder)}")
def save_new_folder_name():
if os.path.exists(backupfolder_input.value):
write_adminsetting("backup_folder", backupfolder_input.value)
ui.notify("Neuen Pfad gespeichert")
else:
with ui.dialog() as dialog, ui.card():
ui.label("Der Pfad")
ui.label(backupfolder_input.value)
ui.label("exisitiert nicht und kann daher nicht verwendet werden.")
ui.button("OK", on_click=dialog.close)
dialog.open()
ui.button("Speichern", on_click=save_new_folder_name).tooltip("Hiermit können Sie das Backupverzeichnis ändeern")
ui.markdown("API-Schlüssel:")
backup_api_key_input = ui.input(value=api_key).tooltip("Hier den API-Schlüssel eintragen, der für Backuperzeugung mittels API-Aufruf verwendet werden soll.")
def new_backup_api_key():
backup_api_key_input.value = hashlib.shake_256(bytes(f"{backupfolder}-{datetime.datetime.now().timestamp()}", 'utf-8')).hexdigest(20)
def save_new_api_key():
write_adminsetting("backup_api_key", backup_api_key_input.value)
with ui.grid(columns=2):
ui.button("Neu", on_click=new_backup_api_key).tooltip("Hiermit können Sie einen neuen zufälligen API-Schlüssel erstellen.")
ui.button("Speichern", on_click=save_new_api_key).tooltip("Neuen API-Schlüssel speichern. Der alte API-Schlüssel ist damit sofort ungültig.")
ui.label(f"Der Adresse für den API-Aufruf lautet /api/backup/[API-Schlüssel]").classes('col-span-3')
ui.separator()
ui.markdown('**Backups**')
date_format = '%Y-%m-%d_%H-%M' date_format = '%Y-%m-%d_%H-%M'
searchpath = os.path.join(scriptpath, backupfolder) searchpath = backupfolder
@ui.refreshable @ui.refreshable
def backup_list(): def backup_list():
@ -1259,20 +1293,32 @@ Dies kann nicht rückgängig gemacht werden!''')
os.makedirs(os.path.join(searchpath)) os.makedirs(os.path.join(searchpath))
backup_files = [] backup_files = []
file_and_size = [] file_info = []
with ui.grid(columns='auto auto auto auto auto'): with ui.grid(columns='auto auto auto auto auto auto'):
ui.label("Backupzeitpunkt/Dateiname")
ui.label("Backupgröße")
ui.label("Programmversion")
for i in range(0,3):
ui.space()
for file in os.listdir(searchpath): for file in os.listdir(searchpath):
if file.endswith(".zip"): if file.endswith(".zip"):
file_and_size = [file, os.path.getsize(os.path.join(searchpath, file))] with zipfile.ZipFile(os.path.join(searchpath, file)) as current_archive:
backup_files.append(file_and_size) try:
current_version = current_archive.read("app_version.txt").decode('utf-8')
except KeyError:
current_version = "-"
file_info = [file, os.path.getsize(os.path.join(searchpath, file)), current_version]
backup_files.append(file_info)
backup_files.sort() backup_files.sort()
backup_files.reverse() backup_files.reverse()
if len(backup_files) == 0: if len(backup_files) == 0:
ui.label("Keine Backups vorhanden") ui.label("Keine Backups vorhanden")
for file, size in backup_files: for file, size, version in backup_files:
date_string = file[0:-4] date_string = file[0:-4]
try: try:
date_string_dt = datetime.datetime.strptime(date_string, date_format) date_string_dt = datetime.datetime.strptime(date_string, date_format)
@ -1281,6 +1327,7 @@ Dies kann nicht rückgängig gemacht werden!''')
button_string = date_string button_string = date_string
ui.markdown(button_string) ui.markdown(button_string)
ui.markdown(f'{round(size/1_000_000,2)} MB') ui.markdown(f'{round(size/1_000_000,2)} MB')
ui.markdown(version)
ui.button(icon='download', on_click=lambda file=date_string: ui.download.file( ui.button(icon='download', on_click=lambda file=date_string: ui.download.file(
os.path.join(scriptpath, backupfolder, f'{file}.zip'))).tooltip( os.path.join(scriptpath, backupfolder, f'{file}.zip'))).tooltip(
"Backup herunterladen") "Backup herunterladen")
@ -1333,8 +1380,7 @@ Dies kann nicht rückgängig gemacht werden!''')
async def make_backup(): async def make_backup():
n = ui.notification("Backup wird erzeugt...") n = ui.notification("Backup wird erzeugt...")
compress = zipfile.ZIP_DEFLATED compress = zipfile.ZIP_DEFLATED
filename = os.path.join(scriptpath, backupfolder, filename = os.path.join(searchpath, datetime.datetime.now().strftime(date_format) + '.zip')
datetime.datetime.now().strftime(date_format) + '.zip')
folder = userfolder folder = userfolder
with zipfile.ZipFile(filename, 'w', compress) as target: with zipfile.ZipFile(filename, 'w', compress) as target:
for root, dirs, files in os.walk(folder): for root, dirs, files in os.walk(folder):
@ -1342,6 +1388,7 @@ Dies kann nicht rückgängig gemacht werden!''')
add = os.path.join(root, file) add = os.path.join(root, file)
target.write(add) target.write(add)
target.write(usersettingsfilename) target.write(usersettingsfilename)
target.writestr("app_version.txt", data=app_version)
backup_list.refresh() backup_list.refresh()
n.dismiss() n.dismiss()
ui.notify("Backup erstellt") ui.notify("Backup erstellt")
@ -1361,16 +1408,51 @@ Dies kann nicht rückgängig gemacht werden!''')
if upload: if upload:
content = e.content.read() content = e.content.read()
with open(os.path.join(searchpath, filename), 'wb') as output: temp_file = os.path.join(searchpath, f"temp-{filename}")
with open(temp_file, 'wb') as output:
output.write(content) output.write(content)
with zipfile.ZipFile(temp_file) as temporary_file:
try:
version_in_file = temporary_file.read("app_version.txt").decode('utf-8')
except KeyError:
version_in_file = ""
if version_in_file == app_version:
os.rename(temp_file, os.path.join(searchpath, filename))
ui.notify("Datei hochgeladen") ui.notify("Datei hochgeladen")
backup_list.refresh() backup_list.refresh()
uploader.reset() zip_upload.reset()
else:
with ui.dialog() as dialog, ui.card():
if version_in_file == "":
ui.label("Es wurden keine gültigen Versionsdaten in der Datei gefunden.")
ui.label("Sind sie sicher, dass Sie diese Datei verwenden möchten?").classes('font-bold')
else:
ui.label(f"Die Versionsdaten des Backups zeigen an, dass diese mit der Version {version_in_file} erstellt wurden. Die aktuell verwendete Version ist {app_version}. Ggf. sind die Backupdaten inkompatibel.")
ui.label("Sind Sie sicher, dass Sie diese Daten verwenden möchten?").classes('font-bold')
go_check = ui.checkbox("Ich bin mir sicher.")
with ui.grid(columns=2):
def go_action():
os.rename(temp_file, os.path.join(searchpath, filename))
ui.notify("Datei hochgeladen")
backup_list.refresh()
zip_upload.reset()
dialog.close()
def abort_action():
os.remove(temp_file)
ui.notify("Temporäre Datei gelöscht")
zip_upload.reset()
dialog.close()
ui.button("Ja", on_click=go_action).bind_enabled_from(go_check, 'value')
ui.button("Nein", on_click=abort_action)
dialog.open()
ui.separator() ui.separator()
ui.label("Backup hochladen").classes('font-bold') ui.label("Backup hochladen").classes('font-bold')
ui.label(f"Stellen Sie sicher, dass Sie nur zur aktuellen Programmversion ({app_version}) Backups hochladen.") ui.label(f"Stellen Sie sicher, dass Sie zur aktuellen Programmversion ({app_version}) passende Backups hochladen.")
uploader = ui.upload(on_upload=handle_upload).props('accept=.zip') zip_upload = ui.upload(on_upload=handle_upload).props('accept=.zip')
ui.separator()
# Alternativ zur Loginseite navigieren # Alternativ zur Loginseite navigieren
else: else:

View File

@ -533,68 +533,14 @@ def json_info(api_key: str):
if not found_key: if not found_key:
return { "data": "none"} return { "data": "none"}
@ui.page("/api/backup") @app.get('/api/backup/{api_key}')
def backup(): def backup_api(api_key: str):
try:
admin_auth = app.storage.user['admin_authenticated']
except:
admin_auth = False
if admin_auth:
date_format = '%Y-%m-%d_%H-%M' date_format = '%Y-%m-%d_%H-%M'
searchpath = backupfolder
pageheader("Backup herunterladen")
ui.page_title(f"{app_title} {app_version}")
ui.label("Vorhandene Backups:")
@ui.refreshable
def backup_list():
if not os.path.isdir(os.path.join(scriptpath, backupfolder)):
os.makedirs(os.path.join(scriptpath, backupfolder))
backup_files = [ ]
with ui.grid(columns='auto auto'):
for file in os.listdir(os.path.join(scriptpath, backupfolder)):
if file.endswith(".zip"):
backup_files.append(file)
backup_files.sort()
backup_files.reverse()
if len(backup_files) == 0:
ui.label("Keine Backups vorhanden")
for file in backup_files:
date_string = file[0:-4]
try:
date_string_dt = datetime.strptime(date_string, date_format)
button_string = date_string_dt.strftime('%d.%m.%Y - %H:%M')
except ValueError:
button_string = date_string
ui.button(button_string, on_click=lambda file=date_string: ui.download.file(f'{file}.zip'))
def del_backup_dialog(file):
def del_backup():
os.remove(os.path.join(scriptpath, backupfolder, f'{file}.zip'))
dialog.close()
ui.notify(f'Backupdatei {file}.zip gelöscht')
backup_list.refresh()
with ui.dialog() as dialog, ui.card():
ui.label(f"Soll das Backup {file}.zip wirklich gelöscht werden?")
ui.label("Dies kann nicht rückgänig gemacht werden!").classes('font-bold')
with ui.grid(columns=2):
ui.button("Löschen", on_click=del_backup)
ui.button("Abbrechen", on_click=dialog.close)
dialog.open()
ui.button(icon='delete', on_click=lambda file=date_string: del_backup_dialog(file))
backup_list()
ui.separator()
def make_backup(): def make_backup():
compress = zipfile.ZIP_DEFLATED compress = zipfile.ZIP_DEFLATED
filename = os.path.join(scriptpath, backupfolder, datetime.now().strftime(date_format) + '.zip') filename = os.path.join(searchpath, datetime.now().strftime(date_format) + '.zip')
folder = userfolder folder = userfolder
with zipfile.ZipFile(filename, 'w', compress) as target: with zipfile.ZipFile(filename, 'w', compress) as target:
for root, dirs, files in os.walk(folder): for root, dirs, files in os.walk(folder):
@ -602,9 +548,9 @@ def backup():
add = os.path.join(root, file) add = os.path.join(root, file)
target.write(add) target.write(add)
target.write(usersettingsfilename) target.write(usersettingsfilename)
backup_list.refresh() target.writestr("app_version.txt", data=app_version)
if api_key == load_adminsettings()["backup_api_key"]:
ui.button("Neues Backup erstellen", on_click=make_backup) make_backup()
return {"backup": datetime.now().strftime(date_format), "success": True}
else: else:
login_mask() return {"backup": datetime.now().strftime(date_format), "success": False}

View File

@ -3,6 +3,7 @@
import os import os
from pathlib import Path from pathlib import Path
import hashlib
app_title = "Zeiterfassung" app_title = "Zeiterfassung"
app_version = ("0.0.0") app_version = ("0.0.0")
@ -10,7 +11,7 @@ app_version = ("0.0.0")
# Standardpfade # Standardpfade
scriptpath = str(Path(os.path.dirname(os.path.abspath(__file__))).parent.absolute()) scriptpath = str(Path(os.path.dirname(os.path.abspath(__file__))).parent.absolute())
userfolder = "users" userfolder = "users"
backupfolder = "backup" backupfolder = str(os.path.join(scriptpath, "backup"))
# Dateinamen # Dateinamen
@ -31,9 +32,11 @@ standard_adminsettings = { "admin_user": "admin",
"times_on_touchscreen": True, "times_on_touchscreen": True,
"photos_on_touchscreen": True, "photos_on_touchscreen": True,
"touchscreen": True, "touchscreen": True,
"picure_height": 200, "picture_height": 200,
"button_height": 300, "button_height": 300,
"user_notes": True, "user_notes": True,
"backupfolder": backupfolder,
"backup_api_key": hashlib.shake_256(bytes(backupfolder, 'utf-8')).hexdigest(20),
"holidays": { } "holidays": { }
} }

View File

@ -504,3 +504,16 @@ def load_adminsettings():
return data return data
except: except:
return -1 return -1
# bestimmte Admineinstellungen speichern
def write_adminsetting(key: str, value):
settings_filename = os.path.join(scriptpath, usersettingsfilename)
admin_settings = load_adminsettings()
try:
admin_settings[key] = value
json_data = json.dumps(admin_settings, indent=4)
with open(settings_filename, 'w') as output_file:
output_file.write(json_data)
except KeyError:
print(f"Kein Einstellungsschlüssel {key} vorhanden.")

View File

@ -1,6 +1,6 @@
from datetime import datetime from datetime import datetime
from nicegui import ui, app from nicegui import ui, app, events
from lib.users import * from lib.users import *
from lib.definitions import * from lib.definitions import *
@ -10,6 +10,10 @@ import hashlib
import calendar import calendar
import locale import locale
import platform
from pathlib import Path
from typing import Optional
locale.setlocale(locale.LC_ALL, '') locale.setlocale(locale.LC_ALL, '')
class pageheader: class pageheader:

View File

@ -9,6 +9,8 @@
"picture_height": "100", "picture_height": "100",
"button_height": "120", "button_height": "120",
"user_notes": true, "user_notes": true,
"backup_folder": "/home/alexander/Dokumente/Python/Zeiterfassung/backup",
"backup_api_key": "6fed93dc4a35308b2c073a8a6f3284afe1fb9946",
"holidays": { "holidays": {
"2025-01-01": "Neujahr", "2025-01-01": "Neujahr",
"2025-04-18": "Karfreitag", "2025-04-18": "Karfreitag",

View File

@ -1,68 +0,0 @@
1743966330
1743966416
1744018256
1744018315
1744018470
1744018696
1744100316
1744100330
1744194603
1744196086
1744196347
1744196348
1744196349
1744196350
1744196350
1744196351
1744197304
1744197306
1744197767
1744197768
1744210910
1744210912
1744210913
1744210914
1744211937
1744211939
1744221416
1744221418
1744221436
1744221439
1744221562
1744221565
1744221993
1744222004
1744222029
1744222032
1744259777
1744259780
1744260543
1744260545
1744266752
1744266755
1744266781
1744266782
1744268299
1744268300
1744269834
1744269835
1744269841
1744269877
1744269879
1744269923
1744269924
1744269924
1744269925
1744269926
1744269963
1744269964
1744269964
1744269967
1744269969
1744269970
1744269974
1744269977
1744269978
1744269980
1744270078
1744270084

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB