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:**")
with ui.grid(columns=2):
def save_admin_settings():
output_dict = { }
output_dict["admin_user"] = admin_user.value
write_adminsetting("admin_user", admin_user.value)
if admin_password.value != "":
output_dict["admin_password"] = hash_password(admin_password.value)
write_adminsetting("admin_password", hash_password(admin_password.value))
else:
output_dict["admin_password"] = data["admin_password"]
output_dict["port"] = port.value
output_dict["secret"] = secret
output_dict["touchscreen"] = touchscreen_switch.value
output_dict["times_on_touchscreen"] = timestamp_switch.value
output_dict["photos_on_touchscreen"] = photo_switch.value
output_dict["picture_height"] = picture_height_input.value
output_dict["button_height"] = button_height_input.value
output_dict["user_notes"] = notes_switch.value
output_dict["holidays"] = data["holidays"]
output_dict["version"] = app_version
write_adminsetting("admin_password", data["admin_password"])
write_adminsetting("port", port.value)
write_adminsetting("secret", secret)
write_adminsetting("touchscreen", touchscreen_switch.value)
write_adminsetting("times_on_touchscreen", timestamp_switch.value)
write_adminsetting("photos_on_touchscreen", photo_switch.value)
write_adminsetting("picture_height", picture_height_input.value)
write_adminsetting("button_height", button_height_input.value)
write_adminsetting("user_notes", notes_switch.value)
write_adminsetting("holidays", data["holidays"])
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):
with ui.dialog() as dialog, ui.card():
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()
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'
searchpath = os.path.join(scriptpath, backupfolder)
searchpath = backupfolder
@ui.refreshable
def backup_list():
@ -1259,20 +1293,32 @@ Dies kann nicht rückgängig gemacht werden!''')
os.makedirs(os.path.join(searchpath))
backup_files = []
file_and_size = []
with ui.grid(columns='auto auto auto auto auto'):
file_info = []
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):
if file.endswith(".zip"):
file_and_size = [file, os.path.getsize(os.path.join(searchpath, file))]
backup_files.append(file_and_size)
with zipfile.ZipFile(os.path.join(searchpath, file)) as current_archive:
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.reverse()
if len(backup_files) == 0:
ui.label("Keine Backups vorhanden")
for file, size in backup_files:
for file, size, version in backup_files:
date_string = file[0:-4]
try:
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
ui.markdown(button_string)
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(
os.path.join(scriptpath, backupfolder, f'{file}.zip'))).tooltip(
"Backup herunterladen")
@ -1333,8 +1380,7 @@ Dies kann nicht rückgängig gemacht werden!''')
async def make_backup():
n = ui.notification("Backup wird erzeugt...")
compress = zipfile.ZIP_DEFLATED
filename = os.path.join(scriptpath, backupfolder,
datetime.datetime.now().strftime(date_format) + '.zip')
filename = os.path.join(searchpath, datetime.datetime.now().strftime(date_format) + '.zip')
folder = userfolder
with zipfile.ZipFile(filename, 'w', compress) as target:
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)
target.write(add)
target.write(usersettingsfilename)
target.writestr("app_version.txt", data=app_version)
backup_list.refresh()
n.dismiss()
ui.notify("Backup erstellt")
@ -1361,16 +1408,51 @@ Dies kann nicht rückgängig gemacht werden!''')
if upload:
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)
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")
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.label("Backup hochladen").classes('font-bold')
ui.label(f"Stellen Sie sicher, dass Sie nur zur aktuellen Programmversion ({app_version}) Backups hochladen.")
uploader = ui.upload(on_upload=handle_upload).props('accept=.zip')
ui.label(f"Stellen Sie sicher, dass Sie zur aktuellen Programmversion ({app_version}) passende Backups hochladen.")
zip_upload = ui.upload(on_upload=handle_upload).props('accept=.zip')
ui.separator()
# Alternativ zur Loginseite navigieren
else:

View File

@ -533,68 +533,14 @@ def json_info(api_key: str):
if not found_key:
return { "data": "none"}
@ui.page("/api/backup")
def backup():
try:
admin_auth = app.storage.user['admin_authenticated']
except:
admin_auth = False
if admin_auth:
@app.get('/api/backup/{api_key}')
def backup_api(api_key: str):
date_format = '%Y-%m-%d_%H-%M'
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()
searchpath = backupfolder
def make_backup():
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
with zipfile.ZipFile(filename, 'w', compress) as target:
for root, dirs, files in os.walk(folder):
@ -602,9 +548,9 @@ def backup():
add = os.path.join(root, file)
target.write(add)
target.write(usersettingsfilename)
backup_list.refresh()
ui.button("Neues Backup erstellen", on_click=make_backup)
target.writestr("app_version.txt", data=app_version)
if api_key == load_adminsettings()["backup_api_key"]:
make_backup()
return {"backup": datetime.now().strftime(date_format), "success": True}
else:
login_mask()
return {"backup": datetime.now().strftime(date_format), "success": False}

View File

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

View File

@ -504,3 +504,16 @@ def load_adminsettings():
return data
except:
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 nicegui import ui, app
from nicegui import ui, app, events
from lib.users import *
from lib.definitions import *
@ -10,6 +10,10 @@ import hashlib
import calendar
import locale
import platform
from pathlib import Path
from typing import Optional
locale.setlocale(locale.LC_ALL, '')
class pageheader:

View File

@ -9,6 +9,8 @@
"picture_height": "100",
"button_height": "120",
"user_notes": true,
"backup_folder": "/home/alexander/Dokumente/Python/Zeiterfassung/backup",
"backup_api_key": "6fed93dc4a35308b2c073a8a6f3284afe1fb9946",
"holidays": {
"2025-01-01": "Neujahr",
"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