Dateien sichern: Python-Backup-Skript entwickeln!

Selbst ein Administrator muss heutzutage ein bisschen programmieren können, um Prozesse automatisieren zu können. Und genau da kommt Python ins Spiel. Die Sprache gilt als einsteigerfreundlich und flexibel. Dank der vielen bestehenden Module braucht es oftmals nur wenige Zeilen Code, um zu einem zuverlässigen Skript zu kommen. Gerade System Engineers dürfte das freuen.

Im heutigen Blogbeitrag möchte ich dir zeigen, wie du in Python ein einfaches Backup-Skript anfertigen kannst. Damit wirst du in der Lage sein, Dateien und Verzeichnisse zu sichern und sie dabei sogar noch mittels ZIP-Verfahren zu komprimieren. Wer als Admin lernen möchte, wie man mit Python effiziente Skripte schreibt, ist hier genau richtig.

Was das fertige Backup-Skript kann:

Damit du einen schnellen Überblick darüber erhältst, was ich dir heute in Python vorstellen möchte, habe ich die Schlüsselfunktionen des Backup-Skripts im Folgenden einmal kurz zusammengefasst:

  • Erstellen eines eindeutigen Backup-Ordners mittels Zeitstempel.
  • Rekursive Sicherung von Dateien und Verzeichnissen.
  • Komprimierung des Backup-Verzeichnisses.
  • Löschen des nicht komprimierten Backup-Verzeichnisses.
  • Funktioniert auf Windows- und Linux-Systemen.

Das Skript verwendet die Module shutil, os, datetime und zipfile, um diese Aufgaben auszuführen. Es ist in der Lage, Dateien und Verzeichnisse von einer Quellquelle zu sichern, sie zu komprimieren und in einem Zielverzeichnis abzulegen. Dies kann nützlich sein, um regelmäßige Sicherungen von wichtigen Daten durchzuführen und diese dann auf einer NAS-Freigabe zu speichern.

#!/usr/bin/env python3

import shutil
import os
import datetime
import zipfile

def backup_folder(source_folder, target_folder):
    try:
        timestamp = datetime.datetime.now().strftime("%d.%m.%Y-%H:%M:%S")
        backup_folder = os.path.join(target_folder, f"backup_{timestamp}")
        os.makedirs(backup_folder)
        
        print()
        print(f"Beginne mit der Sicherung nach {backup_folder}.")
        print()
        
        shutil.copytree(source_folder, os.path.join(backup_folder, os.path.basename(source_folder)))

        print(f"Alle Ordner und Dateien wurden unkomprimiert nach: {backup_folder} gesichert.")
        print()

        print(f"Beginne nun mit der Komprimierung von {backup_folder} mittels Zip.")
        print()

        zip_filename = f"backup_{timestamp}.zip"
        with zipfile.ZipFile(os.path.join(target_folder, zip_filename), 'w', zipfile.ZIP_DEFLATED) as zipf:
            for root, dirs, files in os.walk(backup_folder):
                for file in files:
                    file_path = os.path.join(root, file)
                    arcname = os.path.relpath(file_path, backup_folder)
                    zipf.write(file_path, arcname)

        print(f"Das mittels Zip komprimierte Backup-Archiv {zip_filename} wurde erfolgreich erstellt.")
        print()

        shutil.rmtree(backup_folder)
        print(f"Die unkomprimierte Sicherung unter {backup_folder} wurde nun entfernt.")
        print()

    except Exception as e:
        print(f"#######################################################################")
        print(f"Fehler beim Erstellen des Backups: {str(e)}")
        print(f"#######################################################################")

################################################
# Hier bitte noch die Verzeichnisse eintragen: #
################################################

if __name__ == "__main__":
    source_folder = "/home/user/zu-sicherndes-Verzeichnis"
    target_folder = "/home/user/Ablageort-für-Backup"

    backup_folder(source_folder, target_folder)

Wie man so ein Skript erstellt:

Sind die Anforderungen erstmal geklärt worden, dann geht es auch schon mit der Suche nach den notwendigen Python-Modulen los. Im Gegensatz zu Bash oder Powershell kann Python von Haus aus keine Dateisystemoperationen durchführen. Diese Funktionen rüstet man mithilfe der Library Shutil nach. Ebenfalls wird noch ein Modul namens OS benötigt. Hier ist der Name mal wieder Programm.

Ohne OS hätte das Skript keinerlei Möglichkeit Betriebssystemfunktionen zu nutzen. Auch Datetime sollte wieder selbsterklärend sein. Um den Zeitstempel für das Backup-Verzeichnis erstellen zu können, wird dieses Modul benötigt. Zipfile verrät seine Funktion ebenfalls durch den Namen. Genutzt wird es zum Erstellen, Lesen und Manipulieren von ZIP-Archiven in Python.

Der Programm-Code selbst wird dann in eine Funktion gepackt. Damit kann man steuern, wann der eigentliche Code ausgeführt werden soll. Ich möchte beispielsweise sicherstellen, dass das Skript nur bei einem direkten Aufruf seinen Dienst verrichtet. Daher ist ganz am Ende noch eine If-Anweisung eingebaut, die genau das sicherstellt.

Wird ein Skript in Python direkt aufgerufen, also nicht durch ein anderes importiert, entspricht der Wert von __name__ immer dem String „__main__“. Wichtig zu wissen ist hierbei, dass es sich hier um eine spezielle, vordefinierte Variable aus dem Python-Universum handelt. Ebenfalls noch sinnvoll ist die Verwendung von try und except.

Damit wird das Python-Backup-Skript um ein minimales Fehlerhandling erweitert. Sollte es nämlich zu einem unerwarteten Problem kommen, wird die Exception ausgeführt. Bei diesem Skript wird zwar nur eine kleine Fehlermeldung generiert, allerdings kann man auch Code einbauen, der beispielsweise vorhergegangene Schritte rückgängig macht.

Wie das Skript im Detail funktioniert:

Weiter oben habe ich bereits ausführlich beschrieben, wie ich bei der Erstellung von einfachen Python-Skripten vorgehe. Allerdings bringt ein grober Leitfaden recht wenig, wenn man noch nichts in Python programmiert hat. Weder kennt man die Syntax noch die Eigenheiten der Sprache. Deshalb werde ich dir im Folgenden Schritt für Schritt erklären, was jede einzelne Zeile meines Skriptes genau macht.

Interpreter-Deklaration & Python-Module:

#!/usr/bin/env python3

import shutil
import os
import datetime
import zipfile

Anfangen werde ich daher mit der obersten Zeile. Wie der Großteil meiner Leser wissen sollte, treibe ich mein Unwesen ausschließlich auf Linux-Servern. Und hier braucht man eine Shebang-Zeile, um dem OS miteilen zu können, dass es als Interpreter bitte Python 3 nutzen soll. Gleich danach folgt der Import der notwendigen Python-Module.

Funktion mit Fehlerbehandlung erstellen:

def backup_folder(source_folder, target_folder):
    try:
        print("Code zum Ausführen")
    except Exception as e:
        print(f"Fehler aufgetreten: {e}")

Nun kann es auch schon das Erstellen der einzigen Funktion im ganzen Skriptes gehen. Die Rede ist natürlich von backup_folder(source_folder, target_folder). Was gute von schlechten Skripten unterscheidet, sind Fehlerbehandlungen. Aufgrund dessen wurde hier mit try und except gearbeitet. Sobald ein Fehler im Try-Block auftritt, wird der Code im Except-Bereich ausgeführt.

Häufig handelt es sich dabei nur um eine kleine Fehlermeldung. So bekommt der Anwender zumindest mitgeteilt, was genau nicht funktioniert hat. So können Fehler immerhin identifiziert und im Optimalfall verhindert werden. Besser ist allerdings eine umfangreiche Rettungsaktion. Das Reagieren auf Fehler ist zum Beispiel sinnvoll bei der Wiederherstellung eines fehlgeschlagenen Prozesses.

Zeitstempel & Ordnername erzeugen:

timestamp = datetime.datetime.now().strftime("%d.%m.%Y-%H:%M:%S")
backup_folder = os.path.join(target_folder, f"backup_{timestamp}")
os.makedirs(backup_folder)

Um ein erfolgreiches Backup erstellen zu können, benötigt man eine einzigartige Bezeichnung für das Sicherungs-Verzeichnis. In der Praxis kombiniert man daher einen vorgegebenen Namen wie Backup oder Sicherung mit einem Zeitstempel. Als Erstes wird dafür die aktuelle Zeit im Format Tag.Monat.Jahr-Stunde-Minute-Sekunde in die Variable timestamp gespeichert.

Möglich macht das datetime.now() aus dem gleichnamigen Modul datetime. Allerdings passt die Formatierung jetzt noch nicht ganz. Daher wird noch die Funktion strftime(„%d.%m.%Y-%H:%M:%S“) genutzt. Weiter geht es dann mit dem Modul OS. Hier wird os.path.join() verwendet, um sicherzustellen, dass der Dateipfad plattformunabhängig korrekt zusammengesetzt wird.

Hinter target_folder verbirgt sich dann das Zielverzeichnis, in dem das Backup erstellt werden soll. Der Name des Backup-Ordners wird aus der Zeichenfolge „backup_“ und dem zuvor erstellten Zeitstempel erzeugt. Angelegt wird das Verzeichnis mittels os.makedirs(backup_folder). Diese Funktion erstellt alle benötigten Ordner, auch übergeordnete werden dabei nicht vergessen.

Dateien & Ordner rekursiv sichern:

shutil.copytree(source_folder, os.path.join(backup_folder, os.path.basename(source_folder)))

Hier wird der Inhalt des Quellverzeichnisses (source_folder) in das Backup-Verzeichnis (backup_folder) kopiert. Die Funktion shutil.copytree() wird verwendet, um den gesamten Inhalt des Quellverzeichnisses rekursiv in das Zielverzeichnis zu kopieren. Auffällig am Code ist sicherlich, dass der Target-Ordner nicht als einfache Variable übergeben wird.

Stattdessen wird der Pfad innerhalb der Funktion durch os.path.join(backup_folder, os.path.basename(source_folder))) generiert. Die Funktion os.path.basename(source_folder) wird verwendet, um den Namen des Quellverzeichnisses zu extrahieren. Das ermöglicht es, die Struktur des Quellverzeichnisses im Backup-Ordner beizubehalten.

Da die Bezeichnung des Sicherungsziels eindeutig ist, können mehrere Kopien mit diesem Python Backup Skript erstellt werden. Werden also Daten von /home/user/Dokumente gesichert, wird im Ziel ein gleichnamiger Ordner vorhanden sein. Das könnte dann zum Beispiel wie folgt aussehen: /home/user/backup_03.10.2023-10:00:09/Dokumente.

Backup-Ordner mittels ZIP komprimieren:

zip_filename = f"backup_{timestamp}.zip"
        with zipfile.ZipFile(os.path.join(target_folder, zip_filename), 'w', zipfile.ZIP_DEFLATED) as zipf:
            for root, dirs, files in os.walk(backup_folder):
                for file in files:
                    file_path = os.path.join(root, file)
                    arcname = os.path.relpath(file_path, backup_folder)
                    zipf.write(file_path, arcname)

In der ersten Zeile des Codes wird noch der Name für die ZIP-Datei generiert. Da auch hier wieder ein Zeitstempel an den String backup angehängt wird, erhält man eine einzigartige Bezeichnung. Das ZIP-Verzeichnis wird später einen Namen im Format „backup_TT.MM.JJJJ-STUNDE:MINUTE:SEKUNDE.zip“ tragen. Bis jetzt war der Code noch recht simpel. Das wird sich nun aber schnell ändern.

Die Komprimierung eines fertigen Backups lässt sich beim Shell-Scripting um ein Vielfaches leichter umsetzen. Python ist aber eine richtige Hochsprache und benötigt daher für das Öffnen oder Schreiben in Dateien einen Kontextmanager. Damit wird zum Beispiel sichergestellt, dass geöffnete Dateien wieder geschlossen werden. Aber schauen wir uns die zweite Code-Zeile einmal näher an:

 with zipfile.ZipFile(os.path.join(target_folder, zip_filename), 'w', zipfile.ZIP_DEFLATED) as zipf:

Sie öffnet eine ZIP-Datei für den Schreibzugriff (‚w‘) mit dem generierten Namen der Variable zip_filename im Zielordner (target_folder). Das ‚w‘ gibt an, dass eine neue ZIP-Datei erstellt wird, falls sie noch nicht existiert, oder eine vorhandene Datei überschrieben wird. zipfile.ZIP_DEFLATED legt dann noch die Komprimierungsmethode für die ZIP-Datei fest. Verwendet wird die Standardkomprimierung Deflate.

for root, dirs, files in os.walk(backup_folder):
                for file in files:
                    file_path = os.path.join(root, file)
                    arcname = os.path.relpath(file_path, backup_folder)
                    zipf.write(file_path, arcname)

Eine offene, aber noch vollständig leere Zip-Datei gibt es schon mal. Allerdings braucht man für ein funktionierendes Backup den Inhalt aus dem unserem Backup-Ordner. Dieser muss also noch in das ZIP-Verzeichnis geschrieben werden. Für den Anfang müssen wir uns erstmal einen Überblick über die Datenstruktur verschaffen und nutzen dafür die Funktion os.walk(backup_folder).

Hier wird in einer Schleife das Backup-Verzeichnis rekursiv durchsucht. Den Output liest man dann in root, dirs und files ein. Diese Variablen repräsentieren dann die verschiedenen Teile der Verzeichnisstruktur. Root ist dabei der Pfad zum aktuellen Verzeichnis, dirs ist eine Liste aller Unterverzeichnisse und files ist eine Liste aller Dateien im aktuellen Verzeichnis.

Um einen funktionierenden ZIP-Ordner erstellen zu können, muss man zwei Dateipfade für die vorhandenen Files generieren. Einmal benötigt man den absoluten Pfad zur aktuellen Datei. Dieser setzt sich aus dem Verzeichnispfad (root) und dem Dateinamen (file) zusammen. Dieser Pfad landet dann in der Variable file_path.

Hinter arcname verbirgt sich die Bezeichnung, den die Datei innerhalb des ZIP-Archivs haben wird. Dieser wird relativ zum Backup-Verzeichnis erstellt, damit die Verzeichnisstruktur innerhalb des ZIP-Archivs beibehalten wird. Ganz zum Schluss wird die Datei mit dem absoluten und relativen Pfad in den ZIP-Ordner geschrieben. Möglich macht das die Funktion zipf.write(file_path, arcname).

Unkomprimierte Sicherung löschen:

shutil.rmtree(backup_folder)

Um Speicherplatz zu sparen, ist es natürlich ratsam, den unkomprimierten Backup-Ordner wieder zu löschen. Doppelt braucht man die Daten auch nicht auf demselben Medium zu sichern. Deshalb greife ich für diese Dateisystemoperation auf die Shutil-Library zu und nutze die Funktion rmtree(). Natürlich wird auch dieser Arbeitsschritt wieder mit ein paar Ausgaben für den User versehen.

Backup nur bei direktem Skriptaufruf ausführen:

if __name__ == "__main__":
    source_folder = "/home/user/zu-sicherndes-Verzeichnis"
    target_folder = "/home/user/Ablageort-für-Backup"

    backup_folder(source_folder, target_folder)

Man kann Skripte nicht nur direkt aufrufen, sondern auch in ein anderes Programm importieren. In so einem Fall soll das Python-Backup-Skript aber nicht einfach ausgeführt werden. Deshalb habe ich ganz zum Schluss noch eine If-Anweisung eingebaut, die das sicherstellt. In Python gibt es ein spezielles Attribut namens __name__, welches bei einem direkten Skriptaufruf auf __main__ gesetzt wird.

Konkret heißt das, dass die Backup-Funktion bei einem Import in ein anderes Python-Programm nicht einfach so ausgeführt wird. Stattdessen kannst du die Funktion backup_folder dort im Code einsetzen, wo der eigentliche Bedarf besteht. Das mag hier nicht so wichtig erscheinen, kann aber je nach Programm großen Schaden verhindern. Ich empfehle daher diese Vorgehensweise in Python-Skripten.

Von Fabian Wüst

Er ist leidenschaftlicher Open-Source-Benutzer und ein begeisterter Technologie-Enthusiast. Als kreativer Kopf hinter Homelabtopia bringt Fabian hier seine umfangreiche Erfahrung als Linux-Admin ein. Um sicherzustellen, dass du aus seinen Beiträgen den größtmöglichen Nutzen ziehen kannst, führt er ausgiebige Tests durch und errichtet dafür immense Setups.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert