diff --git a/lib/admin.py b/lib/admin.py index 3007868..eb1e955 100644 --- a/lib/admin.py +++ b/lib/admin.py @@ -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) - ui.notify("Datei hochgeladen") - backup_list.refresh() - uploader.reset() + 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() + 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: diff --git a/lib/api.py b/lib/api.py index c1fe991..e047134 100644 --- a/lib/api.py +++ b/lib/api.py @@ -533,78 +533,24 @@ 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: - - 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() - - def make_backup(): - compress = zipfile.ZIP_DEFLATED - filename = os.path.join(scriptpath, backupfolder, datetime.now().strftime(date_format) + '.zip') - folder = userfolder - with zipfile.ZipFile(filename, 'w', compress) as target: - for root, dirs, files in os.walk(folder): - for file in files: - add = os.path.join(root, file) - target.write(add) - target.write(usersettingsfilename) - backup_list.refresh() - - ui.button("Neues Backup erstellen", on_click=make_backup) +@app.get('/api/backup/{api_key}') +def backup_api(api_key: str): + date_format = '%Y-%m-%d_%H-%M' + searchpath = backupfolder + def make_backup(): + compress = zipfile.ZIP_DEFLATED + 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): + for file in files: + add = os.path.join(root, file) + target.write(add) + target.write(usersettingsfilename) + 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() \ No newline at end of file + return {"backup": datetime.now().strftime(date_format), "success": False} \ No newline at end of file diff --git a/lib/definitions.py b/lib/definitions.py index b9f76cd..2c54691 100644 --- a/lib/definitions.py +++ b/lib/definitions.py @@ -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": { } } diff --git a/lib/users.py b/lib/users.py index 7c7982e..a8a0f98 100644 --- a/lib/users.py +++ b/lib/users.py @@ -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.") + diff --git a/lib/web_ui.py b/lib/web_ui.py index deb9af7..5297d2a 100644 --- a/lib/web_ui.py +++ b/lib/web_ui.py @@ -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: diff --git a/settings.json b/settings.json index 2e00471..a900a48 100644 --- a/settings.json +++ b/settings.json @@ -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", diff --git a/users/testuser2/2025-4.txt b/users/testuser2/2025-4.txt deleted file mode 100644 index 8a73420..0000000 --- a/users/testuser2/2025-4.txt +++ /dev/null @@ -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 diff --git a/users/testuser2/photo.jpg b/users/testuser2/photo.jpg deleted file mode 100644 index 57c5a04..0000000 Binary files a/users/testuser2/photo.jpg and /dev/null differ diff --git a/users/testuser2/photo.png b/users/testuser2/photo.png deleted file mode 100644 index bc5a185..0000000 Binary files a/users/testuser2/photo.png and /dev/null differ