Code-Review und eine Frage zu rsync

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
Dennis89
User
Beiträge: 1562
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Abend,

falls jemand Lust hat darf er sehr gerne über meinen Code herfallen und jede noch so kleine Kleinigkeit bemängeln die gefunden werden kann.

Rahmenbedingungen: Ich habe an einem Raspberry Pi zwei externe Festplatten angeschlossen. Eine davon ist immer eingebunden und wir greifen als Netzwerklaufwerk auf die Daten zu. Die zweite Festplatte soll wöchentlich eingebunden werden, dann wird darauf ein Backup der ersten Platte gespeichert und dann wieder ausgebunden.
Da ich nicht immer alles neu sichern will und dann die Festplatte auch ziemlich schnell voll wäre, habe ich mir gedacht, ich mache jährlich ein "volles" Backup und nutze dann wöchentlich Hardlink-Backups. So sollen nur neu hinzugekommene oder geänderte Dateien gesichert werden. Was ich nicht will, es sollen keine Dateien gelöscht werden, wenn diese im zu sichernden Datenbestand nicht mehr da sind.
Ich würde dann jedes Jahr ein Backup extern sichern und dann mit einer leeren Platte wieder von vorne anfangen.
Soweit der Plan.
Unsicher bin ich mir bei meinen 'rsync'-Optionen. Könnt ihr mir sagen, ob ich da alles notwendige berücksichtigt habe?
Was mir nicht gefällt: Jetzt wird als Hardlink-Ordner immer der Ordner benutzt der 7 Tage älter als das aktuelle Datum ist. Das passt ja genau so lange, so lange ich das Backup nicht mal manuell aus irgendeinem Grund anstoße. Ich müsste als Hardlink-Ordner immer den Ordner nehmen, der am jüngsten ist. Ich weis gerade nur nicht wie ich das anstelle ohne 20 Zeilen Code zu brauchen. Hier wäre ich über Tipps sehr dankbar.
Mir wäre auch noch wichtig, das ich jeden möglichen Fehlerfall sinnvoll abdecke. Das sollte ein sehr zuverlässiges Programm werden.

So jetzt aber der Code:

Code: Alles auswählen

#!/usr/bin/env python3

import smtplib
import ssl
from datetime import date, timedelta
from subprocess import run
from email.mime.text import MIMEText
from pathlib import Path

BACKUP_PATH = Path("/media/Data_Backup/")
DATA_PATH = Path("/media/Data/")


def search_old_backups(date_today):
    for folder in BACKUP_PATH.iterdir():
        backup_date = folder.stem.split("-")
        try:
            #
            # Only compare folders that starte with year
            int(backup_date[0])
            if not backup_date[0] == str(date_today).split("-")[0]:
                return "full"
        except ValueError:
            pass
    return "hardlinks"


def make_backup(backup_mode, date_today):
    if backup_mode == "hardlinks":
        hardlink_folder = date_today - timedelta(days=7)
        try:
            run(
                [
                    "rsync",
                    "-vahHA",
                    f"--log-file={BACKUP_PATH}/Logs/{todays_date}.log",
                    f"--link-dest={BACKUP_PATH}/{hardlink_folder}",
                    DATA_PATH,
                    f"{BACKUP_PATH}/{todays_date}",
                ],
                check=True,
            )
            return True, "Hardlinks-Backup"
        except Exception as e:
            return False, e
    elif backup_mode == "full":
        try:
            run(
                [
                    "rsync",
                    "-vahHA",
                    f"--log-file={BACKUP_PATH}/Logs/{todays_date}.log",
                    DATA_PATH,
                    f"{BACKUP_PATH}/{todays_date}",
                ],
                check=True,
            )
            return True, "Full-Backup - Extern sichern!"
        except Exception as e:
            return False, e


def send_mail(state, text):
    message = MIMEText(text)
    message["Subject"] = state
    message["From"] = "xxx@gmx.de"
    message["To"] = "yyy@web.de"

    context = ssl.create_default_context()
    with smtplib.SMTP_SSL("mail.gmx.net", 465, context=context) as server:
        server.login("xxx@gmx.de", "123456789")
        server.sendmail(
            "xxx@gmx.de", "yyy@web.de", message.as_string()
        )


def main():
    try:
        run(["mount", BACKUP_PATH], check=True)
    except:
        send_mail("Backup nicht erfolgreich!", "Mount nicht erfolgreich")
    date_today = date.today()
    backup_mode = search_old_backups(date_today)
    backup_state, text = make_backup(backup_mode, date_today)

    if backup_state:
        send_mail("Backup erfolgreich!", text)
    else:
        send_mail("Backup nicht erfolgreich!", text)
    run(["umount", BACKUP_PATH], check=True)


if __name__ == "__main__":
    main()
    
   

Grüße und Danke vorab
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
nezzcarth
User
Beiträge: 1765
Registriert: Samstag 16. April 2011, 12:47

Möglicherweise ist das nicht das, was du möchtest, aber falls es auch was von der Stange sein darf, könntest du dir mal existierende Lösungen für inkrementelle Backups anschauen, zum Beispiel duplicity oder BorgBackup. Übrigens beide in Python bzw. Cython geschrieben.
Sirius3
User
Beiträge: 18278
Registriert: Sonntag 21. Oktober 2012, 17:20

`search_old_backups` funktioniert genau falsch. Statt bei nicht übereinstimmendem Jahr "full" zurückzuliefern, sollte die Funktion bei übereinstimmendem Jahr "hardlinks" zurückliefern.
Man fummelt nicht aus dem String eines Date-Objekts das Jahr heraus, wenn man es einfach per year abfragen kann.
`not a == b` ist `a != b`.
In `make_backup` hast Du zweimal fast identischen Code.
Pfade stückelt man nicht mit String-Formatierung zusammen. Du hast Path-Objekte, warum benutzt Du sie nicht richtig?
Exceptions sind dazu da, Fehler zurückzumelden, man wandelt sie nicht in ein bool-Rückgabewert um.
Konstanten schreibt man an den Anfang.

Code: Alles auswählen

#!/usr/bin/env python3
import smtplib
from datetime import date
from subprocess import run
from email.mime.text import MIMEText
from pathlib import Path

BACKUP_PATH = Path("/media/Data_Backup/")
DATA_PATH = Path("/media/Data/")

EMAIL_HOST = "mail.gmx.net"
FROM_EMAIL = "xxx@gmx.de"
EMAIL_PASSWORD = "123"
TO_EMAIL = "yyy@gmx.de"

def search_old_backups(date_today):
    if not next(BACKUP_PATH.glob(f'{date_today:%Y}-*'), None):
        return "full"
    else:
        return "hardlinks"


def make_backup(backup_mode, date_today):
    log_file = BACKUP_PATH / "Logs" / f"{date_today:%Y-%m-%d}.log"
    command = [
        "rsync",
        "-vahHA",
        f"--log-file={log_file}",
        DATA_PATH,
        BACKUP_PATH / f"{date_today:%Y-%m-%d}"
    ]
    if backup_mode == "hardlinks":
        hardlink_folder = max(BACKUP_PATH.iterdir())
        command.append(f"--link-dest={hardlink_folder}")
        text = "Hardlinks-Backup"
    else:
        text = "Full-Backup - Extern sichern!"
    run(command, check=True)


def send_mail(state, text):
    message = MIMEText(text)
    message["Subject"] = state
    message["From"] = FROM_EMAIL
    message["To"] = TO_EMAIL
    with smtplib.SMTP_SSL(EMAIL_HOST) as server:
        server.login(FROM_EMAIL, EMAIL_PASSWORD)
        server.sendmail(
            FROM_EMAIL, TO_EMAIL, message.as_string()
        )


def main():
    try:
        run(["mount", BACKUP_PATH], check=True)
        date_today = date.today()
        backup_mode = search_old_backups(date_today)
        text = make_backup(backup_mode, date_today)
        send_mail("Backup erfolgreich!", text)
    except Exception as e:
        send_mail("Backup nicht erfolgreich!", str(e))


if __name__ == "__main__":
    main()
Benutzeravatar
Dennis89
User
Beiträge: 1562
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Morgen,

vielen Dank für eure Antworten.
nezzcarth hat geschrieben: Mittwoch 17. November 2021, 22:43 Möglicherweise ist das nicht das, was du möchtest, aber falls es auch was von der Stange sein darf, könntest du dir mal existierende Lösungen für inkrementelle Backups anschauen, zum Beispiel duplicity oder BorgBackup. Übrigens beide in Python bzw. Cython geschrieben.
Danke, die Links speichere ich mir mal ab, vielleicht kann ich das mal gebrauchen.

@Sirius3: Danke für die Verbesserungen. Soweit verstehe ich deine Änderungen und ärgere mich auch etwas, dass ich das mit dem Pfad-Objekt nicht selbst gemerkt habe.
Bei einer Anmerkung von dir stehe ich aber etwas auf dem Schlauch.
Ich habe im Code stehen:

Code: Alles auswählen

if not backup_date[0] == str(date_today).split("-")[0]:
    return "full"

Wenn ich das mal in Werte ausdrücke würde da stehen:

Code: Alles auswählen

 if not 2020 == 2021:
     return "full" 

Code: Alles auswählen

 if 2020 != 2021:
     return "full" 
Also wenn die Jahreszahlen nicht gleich sind, dann soll ein "full"-Backup gestartet werden. Stehe ich da jetzt gerade voll auf dem Schlauch oder ist das nicht doch richtig?

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 18278
Registriert: Sonntag 21. Oktober 2012, 17:20

Wenn es gar kein Verzeichnis gibt, gibt es auch keins, das Ungleich dem aktuellen Jahr ist. In diesem Fall gibt es in Deiner Implementierung ein hardlinks-Backup.
Benutzeravatar
Dennis89
User
Beiträge: 1562
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für deine Erklärung.
Sirius3 hat geschrieben: Donnerstag 18. November 2021, 09:45 Wenn es gar kein Verzeichnis gibt, gibt es auch keins, das Ungleich dem aktuellen Jahr ist.
Der Einwand ist perfekt, daran hatte ich gar nicht gedacht.

Jetzt hätte ich nur noch eine Frage zu 'next()' bzw. 'glob'. Die Art wie jetzt nach einem Backup aus dem alten Jahr gesucht wird, funktioniert doch nur, wenn gewährleistet ist dass die Ordner absteigend sortiert sind? Das erste Objekt das 'next' aufruft muss das neuste sein. Kann ich davon ausgehen dass das durch 'glob' so sortiert wird oder wie wird das sichergestellt?

Ich kann das heute Abend auch mit Dummy-Ordner testen, aber ich weis dann nicht ob Ergebnis Zufall ist oder nicht.

Ich möchte noch sagen, das ich den Code nicht in Frage stellen will, sondern nur verstehen will das und wieso er immer funktioniert.

Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 18278
Registriert: Sonntag 21. Oktober 2012, 17:20

Die Reihenfolge ist doch egal. Ob jetzt 2021-01 oder 2021-07 als erstes gefunden ist doch egal, wenn ich mich nur 2021 interessiert.
Benutzeravatar
Dennis89
User
Beiträge: 1562
Registriert: Freitag 11. Dezember 2020, 15:13

Das ist egal, ja. Ich habe es vielleicht etwas schlecht beschrieben. Was wäre denn wenn ich in den ersten Wochen des neuen Jahrs nicht dazu komme, die Ordner mit dem Vorjahresbackup von der Platte zu entfernen.
Also nächstes Jahr Ende Januar befindet sich noch 2021-Ordner auf der Festplatte.
Sollte ich für diesen Fall noch ein 'sorted()' einbauen oder vielleicht noch besser, ich benutze 'max()' so wie beim suchen nach dem Hardlink-Ordner?
Oder gibt es noch etwas sinnvolleres?

Als Antwort wäre ich mit Stichwörter zur Lösung auch zufrieden, würde euch den daraus entstandenen Code zeigen.

Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 18278
Registriert: Sonntag 21. Oktober 2012, 17:20

Nächstes Jahr suchst Du doch nur nach Ordernern die mit 2022- anfangen, dann ist es doch egal, ob da noch 2021-Ordner auch da sind.
Benutzeravatar
Dennis89
User
Beiträge: 1562
Registriert: Freitag 11. Dezember 2020, 15:13

Ach mist. Ja klar da habe ich mich wieder verstrickt.

Vielen Dank für deine Hilfe und deine Geduld.

Werde heute Abend mal den ersten Testlauf mit dem Code machen.

Da zum Thema 'rsync' nichts kam, gehe ich davon aus, dass das so seine Richtigkeit hat.

Viele Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Antworten