commit b21bbfff710510fa72826a4744a93d3923d06b69 Author: Roxi Dittberner Date: Wed Mar 4 10:06:30 2026 +0100 Init diff --git a/DataPuller.py b/DataPuller.py new file mode 100755 index 0000000..1a08567 --- /dev/null +++ b/DataPuller.py @@ -0,0 +1,157 @@ +import os +import shutil + +# --- Hier die Pfade definieren --- +QUELL_ORDNER = input("Quellordner: ") +ZIEL_ORDNER = input("Zielordner: ") +# --------------------------------- + + +def hole_datei_infos(verzeichnis): + """Sammelt alle Dateien mit ihrer Größe und dem Änderungsdatum.""" + datei_infos = {} + if not os.path.exists(verzeichnis): + return datei_infos + + for ordner_pfad, _, dateien in os.walk(verzeichnis): + for datei in dateien: + voller_pfad = os.path.join(ordner_pfad, datei) + # Relativen Pfad ermitteln (z. B. "unterordner\dokument.docx") + relativer_pfad = os.path.relpath(voller_pfad, verzeichnis) + try: + stat = os.stat(voller_pfad) + datei_infos[relativer_pfad] = { + "groesse": stat.st_size, + "aenderungsdatum": stat.st_mtime, + } + except Exception: + pass + return datei_infos + + +def main(): + print("=" * 50) + print(" USB-STICK SYNCHRONISATION") + print("=" * 50) + + # Prüfen, ob Laufwerke da sind + if not os.path.exists(QUELL_ORDNER): + print(f"Fehler: Das Schul-Laufwerk ({QUELL_ORDNER}) ist nicht erreichbar!") + input("\nDrücke Enter zum Beenden...") + return + + ziel_laufwerk = os.path.splitdrive(ZIEL_ORDNER)[0] + "\\" + if not os.path.exists(ziel_laufwerk): + print( + f"Fehler: Laufwerk {ziel_laufwerk} nicht gefunden. Ist der USB-Stick eingesteckt?" + ) + input("\nDrücke Enter zum Beenden...") + return + + print("Analysiere Dateien. Bitte warten...\n") + + quelle = hole_datei_infos(QUELL_ORDNER) + ziel = hole_datei_infos(ZIEL_ORDNER) + + neu = [] + geaendert = [] + geloescht = [] + + # Prüfen, was auf P: neu oder geändert wurde + for pfad, info in quelle.items(): + if pfad not in ziel: + neu.append(pfad) + else: + # Wir prüfen die Dateigröße und das Datum (2 Sekunden Toleranz für USB-Sticks) + if ( + info["groesse"] != ziel[pfad]["groesse"] + or abs(info["aenderungsdatum"] - ziel[pfad]["aenderungsdatum"]) > 2 + ): + geaendert.append(pfad) + + # Prüfen, was von P: gelöscht wurde und auf dem Stick auch weg kann + for pfad in ziel: + if pfad not in quelle: + geloescht.append(pfad) + + # Wenn alles aktuell ist + if not neu and not geaendert and not geloescht: + print( + "Alles ist bereits auf dem neuesten Stand! Keine Änderungen erforderlich." + ) + input("\nDrücke Enter zum Beenden...") + return + + # Übersicht ausgeben + print("Folgende Änderungen wurden gefunden:\n") + if neu: + print("NEUE DATEIEN (werden auf den Stick kopiert):") + for d in neu: + print(f" + {d}") + print() + if geaendert: + print("GEÄNDERTE DATEIEN (Inhalt hat sich geändert, werden aktualisiert):") + for d in geaendert: + print(f" ~ {d}") + print() + if geloescht: + print("GELÖSCHTE DATEIEN (sind nicht mehr auf dem Schul-Laufwerk):") + for d in geloescht: + print(f" - {d}") + print() + + # --- SCHRITT 1: KOPIEREN & AKTUALISIEREN --- + if neu or geaendert: + antwort_kopieren = ( + input("Sollen die NEUEN und GEÄNDERTEN Dateien kopiert werden? (ja/nein): ") + .strip() + .lower() + ) + if antwort_kopieren in ["ja", "j", "yes", "y"]: + print("\nKopiere Dateien...") + for pfad in neu + geaendert: + quell_pfad = os.path.join(QUELL_ORDNER, pfad) + ziel_pfad = os.path.join(ZIEL_ORDNER, pfad) + + # Ordnerstruktur auf dem Stick erstellen, falls ein neuer Unterordner da ist + os.makedirs(os.path.dirname(ziel_pfad), exist_ok=True) + + # Datei kopieren + shutil.copy2(quell_pfad, ziel_pfad) + print(f"Erfolgreich kopiert: {pfad}") + else: + print("\nKopieren übersprungen.") + + # --- SCHRITT 2: LÖSCHEN --- + if geloescht: + print("\n" + "!" * 55) + print(" ACHTUNG: Einige Dateien existieren nur auf dem Stick.") + print("!" * 55) + antwort_loeschen = ( + input( + "Sollen die GELÖSCHTEN Dateien nun auch vom Stick ENTFERNT werden? (ja/nein): " + ) + .strip() + .lower() + ) + + if antwort_loeschen in ["ja", "j", "yes", "y"]: + print("\nLösche Dateien...") + for pfad in geloescht: + ziel_pfad = os.path.join(ZIEL_ORDNER, pfad) + try: + os.remove(ziel_pfad) + print(f"Erfolgreich gelöscht: {pfad}") + except Exception as e: + print(f"Fehler beim Löschen von {pfad}: {e}") + else: + print( + "\nLöschen übersprungen. Die Dateien bleiben als Backup auf dem Stick erhalten." + ) + + print("\nSynchronisation abgeschlossen!") + input("\nDrücke Enter zum Beenden...") + + +if __name__ == "__main__": + main() diff --git a/DataPullerLinux.py b/DataPullerLinux.py new file mode 100644 index 0000000..ab9e887 --- /dev/null +++ b/DataPullerLinux.py @@ -0,0 +1,175 @@ +import os +import argparse +import shutil +import filecmp + +# Argument Parsing +parser = argparse.ArgumentParser( + prog='DataPuller', + description='Check differences between folders and update files if needed') + +parser.add_argument('-i', '--input') +parser.add_argument('-o', '--output') +parser.add_argument('-q', '--quiet', action='store_true') +parser.add_argument('-y', '--confirm', action="store_true") +parser.add_argument('-d', '--delete', action="store_true") +parser.add_argument("--dryrun", action="store_true") + +args = parser.parse_args() + +# Paths +if not args.input: + inpath = input("Source Path: ") +else: + inpath = args.input +if not args.output: + outpath = input("Target Path: ") +else: + outpath = args.output + +def get_files(path): + file_info = {} + if not os.path.exists(path): + return file_info + + for folder, _, files in os.walk(path): + for file in files: + full_path = os.path.join(folder, file) + rel_path = os.path.relpath(full_path, path) + try: + stat = os.stat(full_path) + file_info[rel_path] = { + "size": stat.st_size, + "modified": stat.st_mtime, + } + except Exception: + pass + return file_info + +def main(): + startstring = " FOLDER-SYNC 1.0 " + if not args.quiet: + print(len(startstring)*"=" + "\n" + startstring + "\n" + len(startstring)*"=") + + # Test Folder availability + if not os.path.exists((inpath)): + print(f"ERROR: The input folder ({inpath}) is unavailable or non-existant.") + if not args.quiet: + input("\nPress ENTER to exit...") + exit(1) + + if not os.path.exists(outpath): + print(f"ERROR: The output folder ({outpath}] is unavailable or non-existant.") + if not args.quiet: + input("\nPress ENTER to exit...") + exit(1) + + print("Analyzing files. Please wait...\n") + + source = get_files(inpath) + target = get_files(outpath) + + new = [] + changed = [] + deleted = [] + + # Check for file additions and changes + for path, info in source.items(): + if path not in target: + new.append(path) + elif not filecmp.cmp(os.path.join(inpath, path), os.path.join(outpath, path)): + changed.append(path) + + # Check for file deletions + for path in target: + if path not in source: + deleted.append(path) + + if not new and not changed and not deleted: + if not args.quiet: + print("No changes found.") + input("\nPress ENTER to exit...") + exit(0) + + # Print overview + if not args.quiet: + print("Found the following changes:\n") + if new: + print("Added:") + for i in new: + print(f" + {i}") + print() + if changed: + print("Changed:") + for i in changed: + print(f" ~ {i}") + if deleted: + print("Deleted:") + for i in deleted: + print(f" - {i}") + + # Copy and update files + if not args.dryrun: + if new or changed: + if args.confirm: + copy_confirm = "y" + else: + copy_confirm = input("Write new and changed files? (y/N): ").strip().lower() + if copy_confirm in ["ja", "j", "yes", "y"]: + if not args.quiet: + print("\nCopying files...") + for path in new + changed: + source_path = os.path.join(inpath, path) + target_path = os.path.join(outpath, path) + + # Create new folders if needed + os.makedirs(os.path.dirname(target_path), exist_ok=True) + + # Copy file + try: + shutil.copy(source_path, target_path) + if not args.quiet: + print(f"Successfully copied {path}") + except Exception as error: + print(f"Failed to copy {path} with error {error}") + if not args.confirm: + if not input("Continue? (y/N): ") in ["ja", "j", "yes", "y"]: + exit(1) + elif not args.quiet: + print("\nSkipping changes...") + + # Delete files + if not args.dryrun: + if deleted: + alert = " ALERT: Some files only exist at the destination. " + if not args.quiet: + print(f"{len(alert) * '*'}\n{alert}\n{len(alert) * '*'}") + if args.delete: + delete_confirm = "y" + else: + delete_confirm = input("Delete files on the target? (y/N): ").strip().lower() + if delete_confirm in ["ja", "j", "yes", "y"]: + if not args.quiet: + print("\nDeleting files...") + for path in deleted: + target_path = os.path.join(outpath, path) + try: + os.remove(target_path) + if not args.quiet: + print(f"Successfully deleted {path}") + except Exception as error: + print(f"Failed to delete {path} with error {error}") + if not args.confirm: + if not input("Continue? (y/N): ") in ["ja", "j", "yes", "y"]: + exit(1) + elif not args.quiet: + print("\nSkipping deletion...") + + if not args.quiet: + print("\nFolder sync finished!") + input("\nPress ENTER to exit...") + + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..af37dd9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "datapuller" +version = "0.1.0" +description = "Add your description here" +requires-python = ">=3.13" +dependencies = [] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..7b22948 --- /dev/null +++ b/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "datapuller" +version = "0.1.0" +source = { virtual = "." }