Wie erstelle ich ein temporäres Verzeichnis / einen temporären Ordner in Java?


Antworten:


390

Wenn Sie JDK 7 verwenden, verwenden Sie die neue Klasse Files.createTempDirectory , um das temporäre Verzeichnis zu erstellen.

Path tempDirWithPrefix = Files.createTempDirectory(prefix);

Vor JDK 7 sollte dies Folgendes tun:

public static File createTempDirectory()
    throws IOException
{
    final File temp;

    temp = File.createTempFile("temp", Long.toString(System.nanoTime()));

    if(!(temp.delete()))
    {
        throw new IOException("Could not delete temp file: " + temp.getAbsolutePath());
    }

    if(!(temp.mkdir()))
    {
        throw new IOException("Could not create temp directory: " + temp.getAbsolutePath());
    }

    return (temp);
}

Sie können bessere Ausnahmen machen (Unterklasse IOException), wenn Sie möchten.


12
Das ist gefährlich. Es ist bekannt, dass Java Dateien nicht sofort löscht, daher kann mkdir manchmal fehlschlagen
Demiurg

4
@Demiurg Der einzige Fall, dass eine Datei nicht sofort gelöscht wird, ist unter Windows, wenn die Datei bereits geöffnet ist (sie kann beispielsweise von einem Virenscanner geöffnet werden). Haben Sie andere Unterlagen, die Sie anders zeigen können (ich bin neugierig auf solche Dinge :-)? Wenn es regelmäßig passiert, funktioniert der obige Code nicht. Wenn es selten vorkommt, funktioniert das Aufheben des Aufrufs auf den obigen Code, bis das Löschen erfolgt (oder einige maximale Versuche erreicht sind).
TofuBeer

6
@Demiurg Java ist dafür bekannt, Dateien nicht sofort zu löschen. Das stimmt, auch wenn Sie es nicht öffnen. Ein sicherer Weg ist also temp.delete(); temp = new File(temp.getPath + ".d"); temp.mkdir(); ..., temp.delete();.
Xiè Jìléi

102
Dieser Code hat eine Race-Bedingung zwischen delete()und mkdir(): Ein böswilliger Prozess könnte in der Zwischenzeit das Zielverzeichnis erstellen (unter Verwendung des Namens der kürzlich erstellten Datei). Siehe Files.createTempDir()für eine Alternative.
Joachim Sauer

11
Ich mag ! zu auffallen, zu leicht, um es zu übersehen. Ich habe viel Code gelesen, der von Studenten geschrieben wurde ... wenn (! I) häufig genug ist, um nervig zu sein :-)
TofuBeer

182

Die Google Guava-Bibliothek enthält eine Menge hilfreicher Dienstprogramme. Bemerkenswert ist hier die Files-Klasse . Es gibt eine Reihe nützlicher Methoden, darunter:

File myTempDir = Files.createTempDir();

Dies macht genau das, wonach Sie in einer Zeile gefragt haben. Wenn Sie die Dokumentation hier lesen, werden Sie feststellen, dass die vorgeschlagene Anpassung von File.createTempFile("install", "dir")normalerweise Sicherheitslücken verursacht.


Ich frage mich, auf welche Sicherheitslücke Sie sich beziehen. Dieser Ansatz scheint keine Race-Bedingung zu erstellen, da File.mkdir () vermutlich fehlschlägt, wenn das Verzeichnis bereits vorhanden ist (von einem Angreifer erstellt). Ich glaube nicht, dass dieser Aufruf über böswillige Symlinks folgen wird. Können Sie klarstellen, was Sie gemeint haben?
Abb.

3
@abb: Ich kenne die Details der Rennbedingungen nicht, die in der Guava-Dokumentation erwähnt werden. Ich vermute, dass die Dokumentation korrekt ist, da sie das Problem speziell hervorhebt.
Spina

1
@abb Du hast recht. Solange die Rückgabe von mkdir () überprüft wird, ist dies sicher. Der Code, auf den Spina verweist, verwendet diese mkdir () -Methode. grepcode.com/file/repo1.maven.org/maven2/com.google.guava/guava/… . Dies ist nur ein potenzielles Problem auf Unix-Systemen, wenn das Verzeichnis / tmp verwendet wird, da das Sticky-Bit aktiviert ist.
Sarel Botha

@SarelBotha danke, dass du die Lücke hier ausgefüllt hast. Ich hatte mich schon seit einiger Zeit müßig darüber gewundert.
Spina

168

Wenn Sie zum Testen ein temporäres Verzeichnis benötigen und jUnit verwenden, löst @Rulezusammen mit: TemporaryFolderIhr Problem:

@Rule
public TemporaryFolder folder = new TemporaryFolder();

Aus der Dokumentation :

Mit der TemporaryFolder-Regel können Dateien und Ordner erstellt werden, die nach Abschluss der Testmethode (unabhängig davon, ob sie erfolgreich ist oder nicht) gelöscht werden.


Aktualisieren:

Wenn Sie JUnit Jupiter (Version 5.1.1 oder höher) verwenden, können Sie JUnit Pioneer verwenden, das JUnit 5 Extension Pack.

Aus der Projektdokumentation kopiert :

Der folgende Test registriert beispielsweise die Erweiterung für eine einzelne Testmethode, erstellt und schreibt eine Datei in das temporäre Verzeichnis und überprüft deren Inhalt.

@Test
@ExtendWith(TempDirectory.class)
void test(@TempDir Path tempDir) {
    Path file = tempDir.resolve("test.txt");
    writeFile(file);
    assertExpectedFileContent(file);
}

Weitere Informationen im JavaDoc und im JavaDoc von TempDirectory

Gradle:

dependencies {
    testImplementation 'org.junit-pioneer:junit-pioneer:0.1.2'
}

Maven:

<dependency>
   <groupId>org.junit-pioneer</groupId>
   <artifactId>junit-pioneer</artifactId>
   <version>0.1.2</version>
   <scope>test</scope>
</dependency>

Update 2:

Die Annotation @TempDir wurde der JUnit Jupiter 5.4.0-Version als experimentelle Funktion hinzugefügt. Beispiel aus dem JUnit 5-Benutzerhandbuch kopiert :

@Test
void writeItemsToFile(@TempDir Path tempDir) throws IOException {
    Path file = tempDir.resolve("test.txt");

    new ListWriter(file).write("a", "b", "c");

    assertEquals(singletonList("a,b,c"), Files.readAllLines(file));
}

8
Verfügbar seit JUnit 4.7
Eduard Wirch

Funktioniert nicht in JUnit 4.8.2 unter Windows 7! (Berechtigungsproblem)
Ausnahme

2
@CraigRinger: Warum ist es unklug, sich darauf zu verlassen?
Adam Parkin

2
@AdamParkin Ehrlich gesagt erinnere ich mich nicht mehr. Erklärung fehlgeschlagen!
Craig Ringer

1
Der Hauptvorteil dieses Ansatzes besteht darin, dass das Verzeichnis von JUnit verwaltet wird (vor dem Test erstellt und nach dem Test rekursiv gelöscht). Und es funktioniert. Wenn Sie "temporäres Verzeichnis noch nicht erstellt" erhalten, liegt dies möglicherweise daran, dass Sie @Rule vergessen haben oder das Feld nicht öffentlich ist.
Bogdan Calmac

42

Naiv geschriebener Code zur Lösung dieses Problems leidet unter den Rennbedingungen, einschließlich einiger der Antworten hier. In der Vergangenheit konnten Sie sorgfältig über die Rennbedingungen nachdenken und diese selbst schreiben oder eine Bibliothek eines Drittanbieters wie Google Guava verwenden (wie Spinas Antwort vorschlug). Oder Sie konnten fehlerhaften Code schreiben.

Aber ab JDK 7 gibt es gute Nachrichten! Die Java-Standardbibliothek selbst bietet jetzt eine ordnungsgemäß funktionierende (nicht rassige) Lösung für dieses Problem. Sie möchten java.nio.file.Files # createTempDirectory () . Aus der Dokumentation :

public static Path createTempDirectory(Path dir,
                       String prefix,
                       FileAttribute<?>... attrs)
                                throws IOException

Erstellt ein neues Verzeichnis im angegebenen Verzeichnis unter Verwendung des angegebenen Präfixes, um dessen Namen zu generieren. Der resultierende Pfad ist demselben Dateisystem wie das angegebene Verzeichnis zugeordnet.

Die Details, wie der Name des Verzeichnisses aufgebaut ist, sind implementierungsabhängig und daher nicht angegeben. Wenn möglich, wird das Präfix verwendet, um Kandidatennamen zu erstellen.

Dies behebt effektiv den peinlich alten Fehlerbericht im Sun-Bug-Tracker, der nach einer solchen Funktion gefragt hat.


35

Dies ist der Quellcode für Files.createTempDir () der Guava-Bibliothek. Es ist nirgends so komplex, wie Sie vielleicht denken:

public static File createTempDir() {
  File baseDir = new File(System.getProperty("java.io.tmpdir"));
  String baseName = System.currentTimeMillis() + "-";

  for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
    File tempDir = new File(baseDir, baseName + counter);
    if (tempDir.mkdir()) {
      return tempDir;
    }
  }
  throw new IllegalStateException("Failed to create directory within "
      + TEMP_DIR_ATTEMPTS + " attempts (tried "
      + baseName + "0 to " + baseName + (TEMP_DIR_ATTEMPTS - 1) + ')');
}

Standardmäßig:

private static final int TEMP_DIR_ATTEMPTS = 10000;

Siehe hier


28

Nicht verwenden, deleteOnExit()auch wenn Sie es später explizit löschen.

Google 'deleteonexit is evil' für weitere Informationen, aber der Kern des Problems ist:

  1. deleteOnExit() Löscht nur für normale JVM-Abschaltungen, nicht für Abstürze oder das Beenden des JVM-Prozesses.

  2. deleteOnExit() wird nur beim Herunterfahren der JVM gelöscht - nicht gut für lange laufende Serverprozesse, weil:

  3. Das Schlimmste von allen - deleteOnExit()verbraucht Speicher für jeden temporären Dateieintrag. Wenn Ihr Prozess monatelang ausgeführt wird oder in kurzer Zeit viele temporäre Dateien erstellt, verbrauchen Sie Speicher und geben ihn erst frei, wenn die JVM heruntergefahren wird.


1
Wir haben eine JVM, in der Klassen- und JAR-Dateien versteckte Dateien erhalten, die von der JVM erstellt wurden, und das Löschen dieser zusätzlichen Informationen dauert eine Weile. Bei der erneuten Bereitstellung auf Webcontainern, bei denen WARs explodieren, kann die Bereinigung der JVM nach dem Abschluss buchstäblich Minuten dauern, bevor sie nach einigen Stunden beendet wird.
Thorbjørn Ravn Andersen

20

Ab Java 1.7 createTempDirectory(prefix, attrs)und createTempDirectory(dir, prefix, attrs)sind in enthaltenjava.nio.file.Files

Beispiel: File tempDir = Files.createTempDirectory("foobar").toFile();


14

Folgendes habe ich für meinen eigenen Code beschlossen:

/**
 * Create a new temporary directory. Use something like
 * {@link #recursiveDelete(File)} to clean this directory up since it isn't
 * deleted automatically
 * @return  the new directory
 * @throws IOException if there is an error creating the temporary directory
 */
public static File createTempDir() throws IOException
{
    final File sysTempDir = new File(System.getProperty("java.io.tmpdir"));
    File newTempDir;
    final int maxAttempts = 9;
    int attemptCount = 0;
    do
    {
        attemptCount++;
        if(attemptCount > maxAttempts)
        {
            throw new IOException(
                    "The highly improbable has occurred! Failed to " +
                    "create a unique temporary directory after " +
                    maxAttempts + " attempts.");
        }
        String dirName = UUID.randomUUID().toString();
        newTempDir = new File(sysTempDir, dirName);
    } while(newTempDir.exists());

    if(newTempDir.mkdirs())
    {
        return newTempDir;
    }
    else
    {
        throw new IOException(
                "Failed to create temp dir named " +
                newTempDir.getAbsolutePath());
    }
}

/**
 * Recursively delete file or directory
 * @param fileOrDir
 *          the file or dir to delete
 * @return
 *          true iff all files are successfully deleted
 */
public static boolean recursiveDelete(File fileOrDir)
{
    if(fileOrDir.isDirectory())
    {
        // recursively delete contents
        for(File innerFile: fileOrDir.listFiles())
        {
            if(!FileUtilities.recursiveDelete(innerFile))
            {
                return false;
            }
        }
    }

    return fileOrDir.delete();
}

2
Das ist unsicher. Siehe Kommentar von Joachim Sauer in der ersten (ebenso unsicheren) Option. Der richtige Weg, um das Vorhandensein einer Datei oder eines Verzeichnisses zu überprüfen und dann den Dateinamen atomar abzurufen, besteht darin, die Datei oder das Verzeichnis zu erstellen.
Zbyszek

1
@zbyszek javadocs sagen "Die UUID wird unter Verwendung eines kryptografisch starken Pseudozufallszahlengenerators generiert." In Anbetracht dessen, wie ein böswilliger Prozess ein Verzeichnis mit demselben Namen zwischen exist () und mkdirs () erstellt. Wenn ich mir das jetzt anschaue, denke ich, dass mein existierender () Test ein wenig albern sein könnte.
Keith

Keith: UUID ist in diesem Fall nicht entscheidend, ob sie sicher ist oder nicht. Es reicht für die Informationen über den Namen, den Sie abgefragt haben, um irgendwie "zu lecken". Angenommen, die zu erstellende Datei befindet sich in einem NFS-Dateisystem und der Angreifer kann Pakete (passiv) abhören. Oder der Zufallsgeneratorstatus ist durchgesickert. In meinem Kommentar habe ich gesagt, dass Ihre Lösung ebenso unsicher ist wie die akzeptierte Antwort, aber das ist nicht fair: Die akzeptierte ist trivial mit inotify zu besiegen, und diese ist viel schwerer zu besiegen. In einigen Szenarien ist dies jedoch durchaus möglich.
Zbyszek

2
Ich hatte den gleichen Gedanken und implementierte eine Lösung mit zufälligen UUIDs wie dieser. Es gibt keine Prüfung für, nur einen Versuch zu erstellen - ein starkes RNG, das von der randomUUID-Methode verwendet wird, garantiert so gut wie keine Kollisionen (kann zum Generieren von Primärschlüsseln in DB-Tabellen verwendet werden, habe dies selbst gemacht und habe nie eine Kollision gekannt), also ziemlich sicher. Wenn jemand nicht sicher ist, überprüfen Sie stackoverflow.com/questions/2513573/…
brabster

Wenn Sie sich die Implementierung von Java ansehen, generieren sie nur zufällige Namen, bis keine Kollision mehr auftritt. Ihre maximalen Versuche sind unendlich. Wenn also jemand, der böswillig ist, Ihren Datei- / Verzeichnisnamen immer wieder errät, wird er für immer wiederholt. Hier ist ein Link zur Quelle: hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/9fb81d7a2f16/src/share/… Ich dachte, dass es das Dateisystem irgendwie sperren könnte, damit es atomar einen eindeutigen Namen erzeugen könnte und Erstellen Sie das Verzeichnis, aber ich denke, es macht das nicht gemäß dem Quellcode.
Dosentmatter

5

Nun, "createTempFile" erstellt die Datei tatsächlich. Warum also nicht zuerst löschen und dann das mkdir darauf ausführen?


1
Sie sollten immer den Rückgabewert für mkdir () überprüfen. Wenn dies falsch ist, bedeutet dies, dass das Verzeichnis bereits vorhanden war. Dies kann zu Sicherheitsproblemen führen. Überlegen Sie daher, ob dies einen Fehler in Ihrer Anwendung auslösen soll oder nicht.
Sarel Botha

1
Siehe den Hinweis zur Rennbedingung in der anderen Antwort.
Volker Stolz

Das gefällt mir, abgesehen vom Rennen
Martin Wickman

4

Dieser Code sollte einigermaßen gut funktionieren:

public static File createTempDir() {
    final String baseTempPath = System.getProperty("java.io.tmpdir");

    Random rand = new Random();
    int randomInt = 1 + rand.nextInt();

    File tempDir = new File(baseTempPath + File.separator + "tempDir" + randomInt);
    if (tempDir.exists() == false) {
        tempDir.mkdir();
    }

    tempDir.deleteOnExit();

    return tempDir;
}

3
Was ist, wenn das Verzeichnis bereits vorhanden ist und Sie keinen Lese- / Schreibzugriff darauf haben oder wenn es sich um eine reguläre Datei handelt? Sie haben dort auch eine Rennbedingung.
Jeremy Huiskamp

2
Außerdem löscht deleteOnExit keine nicht leeren Verzeichnisse.
Trenton

3

Wie in dieser RFE und ihren Kommentaren erläutert , können Sie tempDir.delete()zuerst anrufen . Oder Sie können dort System.getProperty("java.io.tmpdir")ein Verzeichnis verwenden und erstellen. In jedem Fall sollten Sie daran denken, anzurufen tempDir.deleteOnExit(), da sonst die Datei nicht gelöscht wird, wenn Sie fertig sind.


Heißt diese Eigenschaft nicht "java.io.tmpdir", nicht "... temp"? Siehe java.sun.com/j2se/1.4.2/docs/api/java/io/File.html
Andrew Swan

Ganz so. Ich hätte es überprüfen sollen, bevor ich wiederhole, was ich gelesen habe.
Michael Myers

Das java.io.tmpdir wird gemeinsam genutzt, daher müssen Sie alle üblichen Voodoos ausführen, um zu vermeiden, dass Sie anderen Personen auf die Zehen treten.
Thorbjørn Ravn Andersen

3

Nur zur Vervollständigung ist dies der Code aus der Google Guava-Bibliothek. Es ist nicht mein Code, aber ich denke, es ist wertvoll, ihn hier in diesem Thread zu zeigen.

  /** Maximum loop count when creating temp directories. */
  private static final int TEMP_DIR_ATTEMPTS = 10000;

  /**
   * Atomically creates a new directory somewhere beneath the system's temporary directory (as
   * defined by the {@code java.io.tmpdir} system property), and returns its name.
   *
   * <p>Use this method instead of {@link File#createTempFile(String, String)} when you wish to
   * create a directory, not a regular file. A common pitfall is to call {@code createTempFile},
   * delete the file and create a directory in its place, but this leads a race condition which can
   * be exploited to create security vulnerabilities, especially when executable files are to be
   * written into the directory.
   *
   * <p>This method assumes that the temporary volume is writable, has free inodes and free blocks,
   * and that it will not be called thousands of times per second.
   *
   * @return the newly-created directory
   * @throws IllegalStateException if the directory could not be created
   */
  public static File createTempDir() {
    File baseDir = new File(System.getProperty("java.io.tmpdir"));
    String baseName = System.currentTimeMillis() + "-";

    for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
      File tempDir = new File(baseDir, baseName + counter);
      if (tempDir.mkdir()) {
        return tempDir;
      }
    }
    throw new IllegalStateException(
        "Failed to create directory within "
            + TEMP_DIR_ATTEMPTS
            + " attempts (tried "
            + baseName
            + "0 to "
            + baseName
            + (TEMP_DIR_ATTEMPTS - 1)
            + ')');
  }

2

Ich habe das gleiche Problem, daher ist dies nur eine weitere Antwort für diejenigen, die interessiert sind, und es ähnelt einer der oben genannten:

public static final String tempDir = System.getProperty("java.io.tmpdir")+"tmp"+System.nanoTime();
static {
    File f = new File(tempDir);
    if(!f.exists())
        f.mkdir();
}

Und für meine Anwendung habe ich beschlossen, eine Option hinzuzufügen, um die Temperatur beim Beenden zu löschen, also habe ich einen Shutdown-Hook hinzugefügt:

Runtime.getRuntime().addShutdownHook(new Thread() {
        @Override
        public void run() {
            //stackless deletion
            String root = MainWindow.tempDir;
            Stack<String> dirStack = new Stack<String>();
            dirStack.push(root);
            while(!dirStack.empty()) {
                String dir = dirStack.pop();
                File f = new File(dir);
                if(f.listFiles().length==0)
                    f.delete();
                else {
                    dirStack.push(dir);
                    for(File ff: f.listFiles()) {
                        if(ff.isFile())
                            ff.delete();
                        else if(ff.isDirectory())
                            dirStack.push(ff.getPath());
                    }
                }
            }
        }
    });

Die Methode löschen Sie alle subdirs und Dateien vor dem Löschen von Temp , ohne die Aufrufhierarchie verwendet (die völlig optional ist und man könnte es mit Rekursion an dieser Stelle tun), aber ich möchte auf der sicheren Seite sein.


2

Wie Sie in den anderen Antworten sehen können, ist kein Standardansatz entstanden. Daher haben Sie bereits Apache Commons erwähnt. Ich schlage den folgenden Ansatz mit FileUtils von Apache Commons IO vor :

/**
 * Creates a temporary subdirectory in the standard temporary directory.
 * This will be automatically deleted upon exit.
 * 
 * @param prefix
 *            the prefix used to create the directory, completed by a
 *            current timestamp. Use for instance your application's name
 * @return the directory
 */
public static File createTempDirectory(String prefix) {

    final File tmp = new File(FileUtils.getTempDirectory().getAbsolutePath()
            + "/" + prefix + System.currentTimeMillis());
    tmp.mkdir();
    Runtime.getRuntime().addShutdownHook(new Thread() {

        @Override
        public void run() {

            try {
                FileUtils.deleteDirectory(tmp);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    });
    return tmp;

}

Dies wird bevorzugt, da Apache die Bibliothek gemeinsam nutzt, die dem angeforderten "Standard" am nächsten kommt und sowohl mit JDK 7 als auch mit älteren Versionen funktioniert. Dies gibt auch eine "alte" Dateiinstanz (die streambasiert ist) und keine "neue" Pfadinstanz zurück (die pufferbasiert ist und das Ergebnis der getTemporaryDirectory () -Methode von JDK7 wäre) -> Daher wird zurückgegeben, was die meisten Benutzer wann benötigen Sie möchten ein temporäres Verzeichnis erstellen.


1

Ich mag die mehrfachen Versuche, einen eindeutigen Namen zu erstellen, aber selbst diese Lösung schließt eine Rennbedingung nicht aus. Ein anderer Prozess kann nach dem Test für exists()und dem if(newTempDir.mkdirs())Methodenaufruf einspringen. Ich habe keine Ahnung, wie ich dies vollständig sicher machen kann, ohne auf nativen Code zurückzugreifen, von dem ich annehme, dass er darin vergraben ist File.createTempFile().


1

Vor Java 7 konnten Sie auch:

File folder = File.createTempFile("testFileUtils", ""); // no suffix
folder.delete();
folder.mkdirs();
folder.deleteOnExit();

1
Schöner Code. Leider funktioniert "deleteOnExit ()" nicht, da Java nicht den gesamten Ordner auf einmal löschen kann. Sie müssen alle Dateien rekursiv löschen: /
Adam Taras

1

Versuchen Sie dieses kleine Beispiel:

Code:

try {
    Path tmpDir = Files.createTempDirectory("tmpDir");
    System.out.println(tmpDir.toString());
    Files.delete(tmpDir);
} catch (IOException e) {
    e.printStackTrace();
}


Importe:
java.io.IOException
java.nio.file.Files
java.nio.file.Path

Konsolenausgabe auf Windows-Computern:
C: \ Benutzer \ Benutzername \ AppData \ Local \ Temp \ tmpDir2908538301081367877

Kommentar:
Files.createTempDirectory generiert atomatisch eine eindeutige ID - 2908538301081367877.

Hinweis:
Lesen Sie Folgendes, um Verzeichnisse rekursiv zu löschen:
Löschen Sie Verzeichnisse rekursiv in Java


0

Die Verwendung von File#createTempFileund deletezum Erstellen eines eindeutigen Namens für das Verzeichnis scheint in Ordnung zu sein. Sie sollten ein hinzufügen ShutdownHook, um das Verzeichnis (rekursiv) beim Herunterfahren von JVM zu löschen.


Ein Abschalthaken ist umständlich. Würde die Datei # deleteOnExit nicht auch funktionieren?
Daniel Hiller

2
#deleteOnExit hat bei mir nicht funktioniert - ich glaube, es werden keine nicht leeren Verzeichnisse gelöscht.
Muriloq

Ich habe einen Schnelltest mit Java 8 implementiert, aber der temporäre Ordner wurde nicht gelöscht, siehe pastebin.com/mjgG70KG
geri
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.