Rust-Werkstatt
Werkstatt · Fährmann · Etappe 6 von 13

Ganze Ordner kopieren – mit allem, was drin ist

Bisher sagt der Fährmann bei Ordnern höflich, dass er sie noch nicht kopieren kann. Heute lernt er es: Ein Druck auf F5, und ein ganzer Ordner wird kopiert, mit allen Unterordnern, allen Dateien darin und mit denselben Zugriffsrechten wie das Original. Dabei begegnest du wieder einer Funktion, die sich selbst aufruft, und einem kleinen Fragezeichen, das in Rust sehr viel Arbeit spart.

2 Schritte · ca. 35 Minuten · baut auf Etappe 5 auf

Der Fährmann meldet in der Meldungszeile einen erfolgreich kopierten Ordner mit der Anzahl der Dateien
So sieht der Fährmann am Ende dieser Etappe aus.
Vorab

Bevor wir loslegen

Wir machen da weiter, wo Etappe 5 aufgehört hat. Den kompletten Stand findest du dort unter Zwischenstand. Und für diese Etappe gilt ganz besonders: Probier alles in deinem Spielordner aus, nicht in echten Daten.

Wie kopiert man einen Ordner? Ganz einfach, wenn man es in Worten sagt: Leg am Ziel einen neuen, leeren Ordner an. Dann geh alle Einträge im Original durch. Ist es eine Datei, kopier sie. Ist es ein Ordner, dann … kopier den Ordner. Und wie das geht, haben wir ja gerade beschrieben. Die Beschreibung benutzt sich selbst, und genau so sieht auch der Code aus: eine Funktion, die sich für jeden Unterordner selbst aufruft. Das nennt man Rekursion.

Und die Rechte? Unter Linux und macOS hat jede Datei und jeder Ordner Zugriffsrechte: Wer darf lesen, wer darf schreiben, wer darf ausführen? Ein Skript, das ausführbar war, soll es nach dem Kopieren auch noch sein. Die gute Nachricht: fs::copy nimmt die Rechte einer Datei schon von allein mit. Bei Ordnern müssen wir selbst nachhelfen. Wie man Rechte anzeigt und ändert, lernst du dann in Etappe 7.

Schritt 1

Eine Funktion, die Ordner kopiert

Für die Fehlerbehandlung brauchen wir gleich io, den Teil der Standardbibliothek für Ein- und Ausgabe:

src/main.rsganz oben, erste Zeile
use std::fs;use std::fs;
use std::io;use std::path::{Path, PathBuf};

Die neue Funktion kommt ans Ende der Datei, hinter groesse_text:

src/main.rsganz am Ende der Datei
    text.replace('.', ",")
}// Kopiert einen ganzen Ordner mit allem, was darin liegt, und liefert die Zahl der Dateien.
// Die Rechte jeder Datei und jedes Ordners werden dabei mitgenommen.
fn ordner_kopieren(von: &Path, nach: &Path) -> io::Result<u64> {
    fs::create_dir(nach)?;
    let mut dateien = 0;

    for eintrag in fs::read_dir(von)? {
        let eintrag = eintrag?;
        let art = eintrag.file_type()?;
        let ziel = nach.join(eintrag.file_name());

        if art.is_symlink() {
            continue; // Verknüpfungen lassen wir aus
        } else if art.is_dir() {
            dateien += ordner_kopieren(&eintrag.path(), &ziel)?;
        } else {
            fs::copy(eintrag.path(), &ziel)?; // nimmt die Rechte der Datei mit
            dateien += 1;
        }
    }

    // Die Rechte des Ordners erst zum Schluss setzen: Ein schreibgeschützter
    // Ordner ließe sich sonst gar nicht mehr befüllen.
    fs::set_permissions(nach, fs::metadata(von)?.permissions())?;
    Ok(dateien)
}

Bevor wir sie Zeile für Zeile anschauen, zum auffälligsten Zeichen darin: dem Fragezeichen.

Fast jede Zeile hier kann schiefgehen. Der Zielordner lässt sich nicht anlegen, ein Eintrag ist nicht lesbar, die Festplatte ist voll. Jedes Mal liefert Rust ein Result. In Etappe 5 haben wir so etwas mit match behandelt. Das wären hier aber sieben match-Blöcke, und die Funktion wäre kaum noch lesbar. Das ? ist die Abkürzung dafür: „Hat es geklappt, nimm das Ergebnis und mach weiter. Wenn nicht, brich die ganze Funktion ab und gib den Fehler an den Aufrufer weiter.“ Damit das geht, muss die Funktion selbst ein Result zurückgeben. Das ist io::Result<u64>: im Erfolgsfall die Zahl der kopierten Dateien, sonst ein Ein-/Ausgabe-Fehler.

Jetzt die Geschichte der Funktion:

  • Zielordner anlegen: fs::create_dir(nach)?
  • Alle Einträge durchgehen: fs::read_dir(von)? öffnet den Ordner, und jeder einzelne Eintrag ist wieder ein Result, daher let eintrag = eintrag?;. Hier „überschatten“ wir den Namen: Aus dem Result namens eintrag wird der echte Eintrag mit demselben Namen.
  • Was ist es? file_type() sagt, ob es ein Ordner, eine Datei oder eine Verknüpfung (ein Symlink) ist. Verknüpfungen lassen wir aus, denn eine Verknüpfung auf einen übergeordneten Ordner würde uns sonst endlos im Kreis kopieren lassen.
  • Ordner: Die Funktion ruft sich selbst auf, für den Unterordner und sein Gegenstück am Ziel. Was dabei an Dateien zusammenkommt, zählen wir dazu.
  • Datei: fs::copy kopiert sie samt ihren Rechten, und wir zählen eins hoch.
  • Rechte des Ordners: Ganz zum Schluss übertragen wir die Rechte des Originalordners auf die Kopie. Warum erst jetzt? Wäre der Originalordner schreibgeschützt und wir setzten das gleich am Anfang, dürften wir in die Kopie nichts mehr hineinlegen.
  • Fertig: Ok(dateien) meldet den Erfolg mit der Anzahl.
Rekursion mit Abbruch Jede Rekursion braucht ein Ende, sonst ruft sie sich ewig selbst auf. Hier endet sie ganz natürlich: Irgendwann hat jeder Ordner nur noch Dateien und keine Unterordner mehr. Genau deshalb sind die Verknüpfungen gefährlich. Mit ihnen könnte ein Ordner sich selbst als Unterordner enthalten, und das Ende käme nie.
Probelauf Speichern, dann im Terminal:
cargo run
Äußerlich ändert sich nichts. Rust warnt, dass ordner_kopieren nie benutzt wird. Gleich bauen wir sie ein.
Schritt 2

Einbauen und absichern

Jetzt bauen wir die neue Funktion in kopieren ein. Dabei ändert sich die Reihenfolge der Prüfungen, und es kommt eine ganz neue dazu:

src/main.rsin der Funktion kopieren
        let zielordner = ziel.ordner.display().to_string();        self.meldung = if ist_ordner {
            format!("„{}“ ist ein Ordner. Ordner kann der Fährmann noch nicht kopieren.", name)
        } else if nach.exists() {
            format!("„{}“ gibt es in {} schon. Nichts überschrieben.", name, zielordner)
        } else {        self.meldung = if nach.exists() {
            format!("„{}“ gibt es in {} schon. Nichts überschrieben.", name, zielordner)
        } else if ist_ordner && nach.starts_with(&von) {
            format!("„{}“ kann nicht in sich selbst kopiert werden.", name)
        } else if ist_ordner {
            match ordner_kopieren(&von, &nach) {
                Ok(anzahl) => format!(
                    "Ordner kopiert: {} mit {} Dateien → {}",
                    name, anzahl, zielordner
                ),
                Err(fehler) => format!("Ordner kopieren fehlgeschlagen: {}", fehler),
            }
        } else {            match fs::copy(&von, &nach) {
  • Schon da? Diese Prüfung steht jetzt ganz vorn, denn sie gilt für Dateien und Ordner gleichermaßen: Was es am Ziel schon gibt, wird nicht überschrieben.
  • In sich selbst? Stell dir vor, links steht der Balken auf dem Ordner Fotos, und rechts bist du in Fotos hineingegangen. Dann sollte der Fährmann Fotos nach Fotos/Fotos kopieren. Beim Kopieren würde er dort aber die neue Kopie wiederfinden und auch sie kopieren, nach Fotos/Fotos/Fotos, und so weiter, bis die Festplatte voll ist. nach.starts_with(&von) erkennt diese Falle: Beginnt der Zielpfad mit dem Quellpfad, liegt das Ziel innerhalb der Quelle.
  • Ordner kopieren: Ein match über das Ergebnis, genau wie bei den Dateien. Weil die Erfolgsmeldung lang ist, verteilt format! sie auf mehrere Zeilen. Das ist erlaubt, Rust achtet nur auf die Kommas.
Was, wenn mittendrin etwas schiefgeht? Dann bricht ordner_kopieren dank ? sofort ab, und die Meldungszeile sagt, was passiert ist. Was bis dahin kopiert war, bleibt am Ziel liegen. Profi-Programme würden die halbe Kopie wieder aufräumen, aber oft will man sie gerade sehen, um den Fehler zu verstehen. Für unseren Fährmann ist Ehrlichkeit hier wichtiger als Perfektion.
Worauf du achten musst Die alte Meldung „Ordner kann der Fährmann noch nicht kopieren“ fällt komplett weg. Die Zeile match fs::copy(…) und alles darunter bleiben unverändert.
Probelauf Speichern, dann im Terminal:
cargo run
Geh im rechten Panel in einen leeren Ordner, setz links den Balken auf einen Ordner mit Unterordnern und drück F5. Die Meldungszeile sagt, wie viele Dateien kopiert wurden, und rechts erscheint der neue Ordner. Geh mit Enter hinein: Alles ist da. Probier dann die Falle aus: Geh rechts in den Ordner hinein, den du links markiert hast, und drück F5. Der Fährmann weigert sich zu Recht. Wenn du die Rechte prüfen willst, vergleich im Terminal ls -l von Original und Kopie: Die Buchstaben ganz links sind gleich.
Ein ganzer Ordner wurde kopiert
Stand nach Schritt 2: Der Ordner „Fotos“ samt Inhalt ist rechts angekommen
Zwischenstand

Der komplette Code nach Etappe 6

Hier ist der vollständige Stand nach Etappe 6. Nimm ihn zum Vergleichen, wenn irgendwo ein Fehler steckt, den du nicht findest, oder als Einstiegspunkt, wenn du erst bei der nächsten Etappe dazustößt.

Cargo.toml und src/main.rs anzeigen

Cargo.toml

[package]
name = "faehrmann"
version = "0.1.0"
edition = "2024"

[dependencies]
iced = "0.14"

src/main.rs

use std::fs;
use std::io;
use std::path::{Path, PathBuf};

use iced::widget::{column, container, row, text};
use iced::keyboard::{self, Key, key::Named};
use iced::{Background, Border, Color, Element, Fill, Font, Subscription, Task, Theme};

// Die Farben des Norton Commander
const BLAU: Color = Color::from_rgb8(0, 0, 170);
const TUERKIS: Color = Color::from_rgb8(0, 170, 170);
const SCHWARZ: Color = Color::BLACK;
const WEISS: Color = Color::WHITE;
const HELLTUERKIS: Color = Color::from_rgb8(85, 255, 255);
const GELB: Color = Color::from_rgb8(255, 255, 85);

// So viele Einträge passen untereinander in ein Panel
const ZEILEN: usize = 24;

// Ein Eintrag in einem Ordner: eine Datei oder ein Unterordner.
struct Eintrag {
    name: String,
    ist_ordner: bool,
    groesse: u64,
}

fn main() -> iced::Result {
    iced::application(Faehrmann::neu, Faehrmann::update, Faehrmann::view)
        .subscription(Faehrmann::subscription)
        .title("Fährmann")
        .default_font(Font::MONOSPACE)
        .window_size((1000.0, 640.0))
        .run()
}

// Das Gedächtnis des Fährmanns: welcher Ordner offen ist und was darin liegt.
struct Faehrmann {
    links: Panel,
    rechts: Panel,
    aktiv: Seite,
    meldung: String, // was der Fährmann zuletzt zu sagen hatte
}

// Welches der beiden Panels ist gerade aktiv?
#[derive(PartialEq)]
enum Seite {
    Links,
    Rechts,
}

#[derive(Debug, Clone)]
enum Message {
    Hoch,
    Runter,
    Oeffnen,
    Wechseln,
    Kopieren,
    Beenden,
}

impl Faehrmann {
    // Startet im Ordner, aus dem das Programm aufgerufen wurde.
    fn neu() -> Self {
        let ordner = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
        Faehrmann {
            links: Panel::neu(ordner.clone()),
            rechts: Panel::neu(ordner),
            aktiv: Seite::Links,
            meldung: String::from("Willkommen an Bord! Tab wechselt das Panel, F5 kopiert, F10 beendet."),
        }
    }

    fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::Hoch => self.aktives_panel().hoch(),
            Message::Runter => self.aktives_panel().runter(),
            Message::Oeffnen => self.aktives_panel().oeffnen(),
            Message::Wechseln => {
                self.aktiv = match self.aktiv {
                    Seite::Links => Seite::Rechts,
                    Seite::Rechts => Seite::Links,
                };
            }
            Message::Kopieren => self.kopieren(),
            Message::Beenden => return iced::exit(),
        }
        Task::none()
    }

    // Abonniert die Tastatur: Jeder Tastendruck geht durch die Funktion taste.
    fn subscription(&self) -> Subscription<Message> {
        keyboard::listen().filter_map(taste)
    }

    // Liefert das Panel, das gerade aktiv ist, zum Verändern.
    fn aktives_panel(&mut self) -> &mut Panel {
        match self.aktiv {
            Seite::Links => &mut self.links,
            Seite::Rechts => &mut self.rechts,
        }
    }

    // Liefert das Panel, das gerade NICHT aktiv ist, zum Verändern.
    fn inaktives_panel(&mut self) -> &mut Panel {
        match self.aktiv {
            Seite::Links => &mut self.rechts,
            Seite::Rechts => &mut self.links,
        }
    }

    // Liefert beide Panels zum Anschauen: zuerst das aktive, dann das andere.
    fn quelle_und_ziel(&self) -> (&Panel, &Panel) {
        match self.aktiv {
            Seite::Links => (&self.links, &self.rechts),
            Seite::Rechts => (&self.rechts, &self.links),
        }
    }

    // Kopiert die markierte Datei vom aktiven Panel in den Ordner des anderen Panels.
    fn kopieren(&mut self) {
        let (quelle, ziel) = self.quelle_und_ziel();
        let Some(eintrag) = quelle.markierter_eintrag() else {
            return;
        };
        if eintrag.name == ".." {
            return;
        }

        // Alles, was wir brauchen, herausschreiben, solange wir die Panels ausleihen
        let name = eintrag.name.clone();
        let ist_ordner = eintrag.ist_ordner;
        let von = quelle.ordner.join(&name);
        let nach = ziel.ordner.join(&name);
        let zielordner = ziel.ordner.display().to_string();

        self.meldung = if nach.exists() {
            format!("„{}“ gibt es in {} schon. Nichts überschrieben.", name, zielordner)
        } else if ist_ordner && nach.starts_with(&von) {
            format!("„{}“ kann nicht in sich selbst kopiert werden.", name)
        } else if ist_ordner {
            match ordner_kopieren(&von, &nach) {
                Ok(anzahl) => format!(
                    "Ordner kopiert: {} mit {} Dateien → {}",
                    name, anzahl, zielordner
                ),
                Err(fehler) => format!("Ordner kopieren fehlgeschlagen: {}", fehler),
            }
        } else {
            match fs::copy(&von, &nach) {
                Ok(_) => format!("Kopiert: {} → {}", name, zielordner),
                Err(fehler) => format!("Kopieren fehlgeschlagen: {}", fehler),
            }
        };

        self.inaktives_panel().neu_lesen();
    }

    fn view(&self) -> Element<'_, Message> {
        let menuzeile = container(text("  Links   Datei   Befehle   Optionen   Rechts"))
            .width(Fill)
            .style(|_theme: &Theme| container::Style {
                background: Some(Background::Color(TUERKIS)),
                text_color: Some(SCHWARZ),
                ..container::Style::default()
            });

        let panels = row![
            self.links.ansicht(self.aktiv == Seite::Links),
            self.rechts.ansicht(self.aktiv == Seite::Rechts),
        ]
        .spacing(6);

        let arbeitsflaeche = container(panels)
            .width(Fill)
            .height(Fill)
            .padding(6)
            .style(|_theme: &Theme| container::Style {
                background: Some(Background::Color(BLAU)),
                text_color: Some(WEISS),
                ..container::Style::default()
            });

        let meldungszeile = container(text(self.meldung.as_str()).color(WEISS))
            .width(Fill)
            .padding([0, 6])
            .style(|_theme: &Theme| container::Style {
                background: Some(Background::Color(SCHWARZ)),
                ..container::Style::default()
            });

        let tastenleiste = row![
            f_taste(1, "Hilfe"),
            f_taste(2, "Menü"),
            f_taste(3, "Ansicht"),
            f_taste(4, "Bearb."),
            f_taste(5, "Kopie"),
            f_taste(6, "Versch."),
            f_taste(7, "Ordner"),
            f_taste(8, "Lösch."),
            f_taste(9, "Leiste"),
            f_taste(10, "Ende"),
        ]
        .spacing(4);

        let tastenleiste = container(tastenleiste)
            .width(Fill)
            .style(|_theme: &Theme| container::Style {
                background: Some(Background::Color(SCHWARZ)),
                ..container::Style::default()
            });

        column![menuzeile, arbeitsflaeche, meldungszeile, tastenleiste].into()
    }
}

// Ein Panel: ein Ordner, seine Einträge und der Auswahlbalken.
struct Panel {
    ordner: PathBuf,
    eintraege: Vec<Eintrag>,
    cursor: usize,      // Nummer des markierten Eintrags
    erste_zeile: usize, // Nummer des obersten sichtbaren Eintrags
}

impl Panel {
    // Legt ein Panel an, das den angegebenen Ordner zeigt.
    fn neu(ordner: PathBuf) -> Self {
        let eintraege = ordner_lesen(&ordner);
        Panel {
            ordner,
            eintraege,
            cursor: 0,
            erste_zeile: 0,
        }
    }

    // Balken eine Zeile nach oben
    fn hoch(&mut self) {
        if self.cursor > 0 {
            self.cursor -= 1;
        }
        self.sichtbar_halten();
    }

    // Balken eine Zeile nach unten
    fn runter(&mut self) {
        if self.cursor + 1 < self.eintraege.len() {
            self.cursor += 1;
        }
        self.sichtbar_halten();
    }

    // Liefert den Eintrag, auf dem der Balken steht, falls es einen gibt.
    fn markierter_eintrag(&self) -> Option<&Eintrag> {
        self.eintraege.get(self.cursor)
    }

    // Liest den Ordner neu ein, zum Beispiel nachdem eine Datei dazugekommen ist.
    fn neu_lesen(&mut self) {
        self.eintraege = ordner_lesen(&self.ordner);
        if self.cursor >= self.eintraege.len() {
            self.cursor = self.eintraege.len().saturating_sub(1);
        }
        self.sichtbar_halten();
    }

    // Schiebt den sichtbaren Ausschnitt so, dass der Balken immer zu sehen ist.
    fn sichtbar_halten(&mut self) {
        if self.cursor < self.erste_zeile {
            self.erste_zeile = self.cursor;
        }
        if self.cursor >= self.erste_zeile + ZEILEN {
            self.erste_zeile = self.cursor + 1 - ZEILEN;
        }
    }

    // Betritt den markierten Ordner, oder geht bei ".." eine Ebene hoch.
    fn oeffnen(&mut self) {
        let Some(eintrag) = self.eintraege.get(self.cursor) else {
            return;
        };
        if !eintrag.ist_ordner {
            return; // Dateien öffnen wir (noch) nicht
        }

        let neuer_ordner = if eintrag.name == ".." {
            match self.ordner.parent() {
                Some(oben) => oben.to_path_buf(),
                None => return,
            }
        } else {
            self.ordner.join(&eintrag.name)
        };

        self.eintraege = ordner_lesen(&neuer_ordner);
        self.ordner = neuer_ordner;
        self.cursor = 0;
        self.erste_zeile = 0;
    }

    // Zeichnet das Panel. Nur das aktive Panel zeigt den Balken und einen hellen Kopf.
    fn ansicht(&self, aktiv: bool) -> Element<'_, Message> {
        let kopffarbe = if aktiv { SCHWARZ } else { GELB };
        let kopf = container(text(self.ordner.display().to_string()).color(kopffarbe))
            .style(move |_theme: &Theme| container::Style {
                background: if aktiv {
                    Some(Background::Color(TUERKIS))
                } else {
                    None
                },
                ..container::Style::default()
            });

        let mut liste = column![];
        for (nr, eintrag) in self
            .eintraege
            .iter()
            .enumerate()
            .skip(self.erste_zeile)
            .take(ZEILEN)
        {
            liste = liste.push(zeile(eintrag, aktiv && nr == self.cursor));
        }

        container(column![kopf, liste].spacing(6))
            .width(Fill)
            .height(Fill)
            .padding(8)
            .clip(true)
            .style(|_theme: &Theme| container::Style {
                border: Border {
                    color: TUERKIS,
                    width: 2.0,
                    radius: 0.0.into(),
                },
                ..container::Style::default()
            })
            .into()
    }
}

// Baut eine einzelne Taste der Funktionstastenleiste, z. B. "5 Kopie".
fn f_taste(nummer: u8, beschriftung: &'static str) -> Element<'static, Message> {
    let zahl = text(nummer).color(WEISS);

    let feld = container(text(beschriftung))
        .width(Fill)
        .style(|_theme: &Theme| container::Style {
            background: Some(Background::Color(TUERKIS)),
            text_color: Some(SCHWARZ),
            ..container::Style::default()
        });

    row![zahl, feld].width(Fill).into()
}

// Liest einen Ordner und liefert seine Einträge: oben "..", dann Ordner, dann Dateien.
fn ordner_lesen(pfad: &Path) -> Vec<Eintrag> {
    let mut eintraege = Vec::new();

    let Ok(inhalt) = fs::read_dir(pfad) else {
        return eintraege; // Ordner nicht lesbar: leere Liste
    };

    for eintrag in inhalt.flatten() {
        let name = eintrag.file_name().to_string_lossy().to_string();
        if name.starts_with('.') {
            continue; // versteckte Dateien überspringen
        }
        let Ok(info) = eintrag.metadata() else {
            continue; // keine Infos lesbar: Eintrag überspringen
        };
        eintraege.push(Eintrag {
            name,
            ist_ordner: info.is_dir(),
            groesse: info.len(),
        });
    }

    // Ordner vor Dateien, innerhalb davon alphabetisch
    eintraege.sort_by(|a, b| {
        b.ist_ordner
            .cmp(&a.ist_ordner)
            .then(a.name.to_lowercase().cmp(&b.name.to_lowercase()))
    });

    // Der Weg nach oben steht immer ganz oben
    if pfad.parent().is_some() {
        let hoch = Eintrag {
            name: String::from(".."),
            ist_ordner: true,
            groesse: 0,
        };
        eintraege.insert(0, hoch);
    }

    eintraege
}

// Baut eine Zeile im Panel: links der Name, rechts die Größe.
fn zeile(eintrag: &Eintrag, markiert: bool) -> Element<'_, Message> {
    let farbe = if markiert {
        SCHWARZ
    } else if eintrag.ist_ordner {
        WEISS
    } else {
        HELLTUERKIS
    };
    let groesse = if eintrag.ist_ordner {
        String::from("<ORDNER>")
    } else {
        groesse_text(eintrag.groesse)
    };

    let inhalt = row![
        text(eintrag.name.as_str())
            .color(farbe)
            .width(Fill)
            .wrapping(text::Wrapping::None),
        text(groesse).color(farbe),
    ]
    .spacing(12);

    // Der markierte Eintrag bekommt den türkisen Balken
    container(inhalt)
        .width(Fill)
        .style(move |_theme: &Theme| container::Style {
            background: if markiert {
                Some(Background::Color(TUERKIS))
            } else {
                None
            },
            ..container::Style::default()
        })
        .into()
}

// Macht aus einer Zahl von Bytes etwas Lesbares, zum Beispiel "12,3 KB".
fn groesse_text(bytes: u64) -> String {
    let b = bytes as f64;
    let text = if bytes < 1024 {
        format!("{} B", bytes)
    } else if b < 1024.0 * 1024.0 {
        format!("{:.1} KB", b / 1024.0)
    } else if b < 1024.0 * 1024.0 * 1024.0 {
        format!("{:.1} MB", b / 1024.0 / 1024.0)
    } else {
        format!("{:.1} GB", b / 1024.0 / 1024.0 / 1024.0)
    };
    text.replace('.', ",")
}

// Kopiert einen ganzen Ordner mit allem, was darin liegt, und liefert die Zahl der Dateien.
// Die Rechte jeder Datei und jedes Ordners werden dabei mitgenommen.
fn ordner_kopieren(von: &Path, nach: &Path) -> io::Result<u64> {
    fs::create_dir(nach)?;
    let mut dateien = 0;

    for eintrag in fs::read_dir(von)? {
        let eintrag = eintrag?;
        let art = eintrag.file_type()?;
        let ziel = nach.join(eintrag.file_name());

        if art.is_symlink() {
            continue; // Verknüpfungen lassen wir aus
        } else if art.is_dir() {
            dateien += ordner_kopieren(&eintrag.path(), &ziel)?;
        } else {
            fs::copy(eintrag.path(), &ziel)?; // nimmt die Rechte der Datei mit
            dateien += 1;
        }
    }

    // Die Rechte des Ordners erst zum Schluss setzen: Ein schreibgeschützter
    // Ordner ließe sich sonst gar nicht mehr befüllen.
    fs::set_permissions(nach, fs::metadata(von)?.permissions())?;
    Ok(dateien)
}

// Übersetzt einen Tastendruck in eine Nachricht, oder in gar nichts.
fn taste(ereignis: keyboard::Event) -> Option<Message> {
    let keyboard::Event::KeyPressed { key, .. } = ereignis else {
        return None; // Taste losgelassen oder Ähnliches: interessiert uns nicht
    };

    match key {
        Key::Named(Named::ArrowUp) => Some(Message::Hoch),
        Key::Named(Named::ArrowDown) => Some(Message::Runter),
        Key::Named(Named::Enter) => Some(Message::Oeffnen),
        Key::Named(Named::Tab) => Some(Message::Wechseln),
        Key::Named(Named::F5) => Some(Message::Kopieren),
        Key::Named(Named::F10) => Some(Message::Beenden),
        _ => None,
    }
}
ZusammenfassungDas ? gibt einen Fehler sofort an den Aufrufer weiter und erspart viele match-Blöcke, setzt aber voraus, dass die Funktion selbst ein Result liefert, etwa io::Result<T>. Eine rekursive Funktion kopiert verschachtelte Ordner beliebiger Tiefe. file_type() unterscheidet Ordner, Dateien und Verknüpfungen, fs::copy überträgt die Rechte einer Datei, und mit fs::set_permissions setzen wir sie für Ordner selbst. starts_with schützt vor dem Kopieren eines Ordners in sich selbst.
Übungen

Zum Ausprobieren

Die Übungen sind freiwillig, aber sie helfen enorm. Mach nach jeder Übung einen Probelauf, und stell danach den alten Zustand wieder her, damit die nächste Etappe bei dir genauso weitergeht wie bei allen anderen.

  1. Wie groß? Lass ordner_kopieren statt der Zahl der Dateien die Zahl der kopierten Bytes liefern und zeig sie mit groesse_text an. Tipp: fs::copy liefert die Bytes einer Datei.
  2. Verknüpfungen zählen: Der Fährmann überspringt Verknüpfungen stillschweigend. Wie könnte er am Ende sagen, wie viele er ausgelassen hat? Überleg dir, welche Rückgabe dafür nötig wäre.
  3. Ohne Fragezeichen: Schreib die Zeile fs::create_dir(nach)?; einmal mit match aus. Wie viele Zeilen brauchst du?
Lösungen anzeigen

1. dateien += 1; wird zu bytes += fs::copy(eintrag.path(), &ziel)?; (die Variable heißt dann besser bytes), und die Rekursion zählt ihre Bytes genauso dazu. In kopieren: Ok(bytes) => format!("Ordner kopiert: {} ({}) → {}", name, groesse_text(bytes), zielordner).

2. Die Funktion müsste zwei Zahlen liefern, zum Beispiel als Tupel io::Result<(u64, u64)> mit Dateien und ausgelassenen Verknüpfungen. Bei der Rekursion zählt man beide Werte des Unterordners dazu: let (d, v) = ordner_kopieren(…)?; dateien += d; verknuepfungen += v;.

3. Etwa so: match fs::create_dir(nach) { Ok(()) => {} Err(fehler) => return Err(fehler), }, also vier oder fünf Zeilen statt einer. Und genau das macht das Fragezeichen für dich. Mehr zu Result und ?: Buch, Kapitel 52.

Etappe 6 geschafft 🗂️

Der Fährmann kopiert jetzt ganze Ordner, beliebig tief verschachtelt, mit allen Rechten und mit einer Sicherung gegen die gefährlichste Falle. In der nächsten Etappe machen wir die Rechte sichtbar und bauen einen kleinen Dialog im Commander-Stil, mit dem du sie ändern kannst.

Hat dir das Mitbauen Spaß gemacht? Ein Kaffee hilft, dass die nächste Etappe ein Stück schneller fertig wird.

☕ Kaffee spendieren