Jak utworzyć tymczasowy katalog / folder w Javie?

Odpowiedzi:

390

Jeśli używasz JDK 7, użyj nowej klasy Files.createTempDirectory, aby utworzyć katalog tymczasowy.

Path tempDirWithPrefix = Files.createTempDirectory(prefix);

Przed JDK 7 powinno to zrobić:

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);
}

Możesz zrobić lepsze wyjątki (podklasa IOException), jeśli chcesz.

TofuBeer
źródło
12
To jest niebezpieczne. Wiadomo, że Java nie usuwa plików natychmiast, więc mkdir może czasami zawieść
Demiurg
4
@Demiurg Jedyny przypadek, w którym plik nie jest natychmiast usuwany, występuje w systemie Windows, gdy plik jest już otwarty (może zostać otwarty na przykład przez skaner antywirusowy). Czy masz inną dokumentację do pokazania inaczej (jestem ciekawy takich rzeczy :-)? Jeśli zdarza się to regularnie, powyższy kod nie będzie działał, jeśli jest to rzadkie, zadziałanie odkładanie połączenia z powyższym kodem, dopóki nie nastąpi usunięcie (lub osiągnie się maksymalną liczbę prób).
TofuBeer,
6
@Demiurg Java nie usuwa plików natychmiast. To prawda, nawet jeśli go nie otworzysz. Tak więc bezpieczniejszy jest sposób temp.delete(); temp = new File(temp.getPath + ".d"); temp.mkdir(); ..., temp.delete();.
Xiè Jìléi
102
Ten kod ma warunek wyścigu pomiędzy delete()i mkdir(): W międzyczasie złośliwy proces może utworzyć katalog docelowy (przyjmując nazwę niedawno utworzonego pliku). Sprawdź Files.createTempDir()alternatywę.
Joachim Sauer
11
Lubię ! aby się wyróżniać, zbyt łatwo to przeoczyć. Czytam dużo kodu napisanego przez studentów ... jeśli (! I) jest na tyle powszechny, że może być denerwujący :-)
TofuBeer
182

Biblioteka Google Guava ma mnóstwo przydatnych narzędzi. Jedną z ważniejszych tutaj jest klasa Files . Ma wiele przydatnych metod, w tym:

File myTempDir = Files.createTempDir();

Robi dokładnie to, o co prosiłeś w jednym wierszu. Jeśli przeczytasz tutaj dokumentację , zobaczysz, że proponowana adaptacja File.createTempFile("install", "dir")zwykle wprowadza luki w zabezpieczeniach.

Spina
źródło
Zastanawiam się, do której luki się odwołujesz. Podejście to nie wydaje się tworzyć warunku wyścigu, ponieważ File.mkdir () prawdopodobnie zawiedzie, jeśli taki katalog już istnieje (utworzony przez atakującego). Nie sądzę, że to połączenie będzie przechodzić przez złośliwe dowiązania symboliczne. Czy możesz wyjaśnić, co miałeś na myśli?
ab
3
@abb: Nie znam szczegółów stanu wyścigu, które są wymienione w dokumentacji Guava. Podejrzewam, że dokumentacja jest poprawna, biorąc pod uwagę, że konkretnie wywołuje problem.
Spina,
1
@abb Masz rację. Dopóki sprawdzany jest zwrot mkdir (), jest bezpieczny. Kod Spina wskazuje na użycie tej metody mkdir (). grepcode.com/file/repo1.maven.org/maven2/com.google.guava/guava/… . Jest to tylko potencjalny problem w systemach Unix podczas korzystania z katalogu / tmp, ponieważ ma włączony bit lepki.
Sarel Botha
@SarelBotha dziękuję za wypełnienie pustego miejsca tutaj. Długo się nad tym zastanawiałem.
Spina
168

Jeśli potrzebujesz tymczasowego katalogu do testowania i używasz jUnit, @Rulerazem z TemporaryFolderrozwiązaniem problemu:

@Rule
public TemporaryFolder folder = new TemporaryFolder();

Z dokumentacji :

Reguła TemporaryFolder umożliwia tworzenie plików i folderów, które z pewnością zostaną usunięte po zakończeniu metody testowej (niezależnie od tego, czy przejdzie, czy nie)


Aktualizacja:

Jeśli używasz JUnit Jupiter (wersja 5.1.1 lub nowsza), możesz skorzystać z JUnit Pioneer, który jest pakietem rozszerzeń JUnit 5.

Skopiowano z dokumentacji projektu :

Na przykład poniższy test rejestruje rozszerzenie dla pojedynczej metody testowej, tworzy i zapisuje plik w katalogu tymczasowym oraz sprawdza jego zawartość.

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

Więcej informacji w JavaDoc i JavaDoc w TempDirectory

Stopień:

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>

Aktualizacja 2:

@TempDir Adnotacja dodano do JUnit Jupiter 5.4.0 uwalniania jako funkcję doświadczalnej. Przykład skopiowany z Podręcznika użytkownika JUnit 5 :

@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));
}
matsev
źródło
8
Dostępne od JUnit 4.7
Eduard Wirch
Nie działa w JUnit 4.8.2 w systemie Windows 7! (Problem z uprawnieniami)
wyjątek
2
@CraigRinger: Dlaczego nie można na tym polegać?
Adam Parkin,
2
@AdamParkin Szczerze mówiąc, już nie pamiętam. Wyjaśnienie nie powiodło się!
Craig Ringer
1
Główną zaletą tego podejścia jest to, że katalog jest zarządzany przez JUnit (utworzony przed testem i usunięty rekurencyjnie po teście). I to działa. Jeśli pojawi się komunikat „temp dir not not yet”, może to oznaczać, że zapomniałeś @Rule lub pole jest niepubliczne.
Bogdan Calmac
42

Naiwnie napisany kod rozwiązujący ten problem cierpi z powodu warunków wyścigowych, w tym kilku odpowiedzi tutaj. Historycznie można było dokładnie przemyśleć warunki wyścigu i napisać je samodzielnie, lub użyć biblioteki innej firmy, takiej jak Google Guava (jak sugeruje odpowiedź Spiny). Możesz też napisać błędny kod.

Ale od JDK 7 są dobre wieści! Sama standardowa biblioteka Java zapewnia teraz poprawnie działające (nieratyczne) rozwiązanie tego problemu. Chcesz java.nio.file.Files # createTempDirectory () . Z dokumentacji :

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

Tworzy nowy katalog w określonym katalogu, używając podanego prefiksu do wygenerowania jego nazwy. Wynikowa ścieżka jest powiązana z tym samym systemem plików co dany katalog.

Szczegóły budowy nazwy katalogu zależą od implementacji i dlatego nie są określone. Tam, gdzie to możliwe, przedrostek służy do konstruowania nazw kandydatów.

To skutecznie rozwiązuje zawstydzająco starożytny raport o błędach w module śledzenia błędów Sun, który poprosił o taką właśnie funkcję.

Greg Price
źródło
35

To jest kod źródłowy Files.createTempDir biblioteki Guava. Nie jest to tak skomplikowane, jak mogłoby się wydawać:

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) + ')');
}

Domyślnie:

private static final int TEMP_DIR_ATTEMPTS = 10000;

Spójrz tutaj

Andres Kievsky
źródło
28

Nie używaj, deleteOnExit()nawet jeśli później usuniesz go wyraźnie.

Google „deleteonexit is evil”, aby uzyskać więcej informacji, ale sedno problemu to:

  1. deleteOnExit() usuwa tylko w przypadku normalnego zamykania JVM, nie powoduje awarii ani nie zabija procesu JVM.

  2. deleteOnExit() usuwa tylko przy zamykaniu JVM - nie nadaje się do długotrwałych procesów serwera, ponieważ:

  3. Najbardziej zło ze wszystkich - deleteOnExit()zużywa pamięć dla każdego wpisu pliku tymczasowego. Jeśli proces trwa kilka miesięcy lub w krótkim czasie utworzysz wiele plików tymczasowych, zużywasz pamięć i nigdy jej nie zwalniasz, dopóki JVM nie wyłączy się.

Deweloper
źródło
1
Mamy maszynę JVM, w której pliki klas i jar są ukryte pod spodem przez JVM, a usunięcie tych dodatkowych informacji zajmuje sporo czasu. Podczas przeprowadzania gorących ponownych wdrożeń w kontenerach sieciowych eksplodujących WAR, JVM może dosłownie poświęcić kilka minut na oczyszczenie po zakończeniu, ale przed wyjściem po kilku godzinach pracy.
Thorbjørn Ravn Andersen
20

Od wersji Java 1.7 createTempDirectory(prefix, attrs)i createTempDirectory(dir, prefix, attrs)są zawarte wjava.nio.file.Files

Przykład: File tempDir = Files.createTempDirectory("foobar").toFile();

poszukiwacz
źródło
14

Oto, co postanowiłem zrobić dla własnego kodu:

/**
 * 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();
}
Keith
źródło
2
To jest niepewne. Zobacz komentarz Joachima Sauera w pierwszej (równie niepewnej) opcji. Prawidłowym sposobem sprawdzenia istnienia pliku lub katalogu i atomowego pobrania pliku jest utworzenie pliku lub katalogu.
zbyszek
1
@zbyszek javadocs powiedział: „UUID jest generowany przy użyciu silnie kryptograficznie generatora liczb pseudolosowych”. Biorąc pod uwagę, że w jaki sposób złośliwy proces tworzy katalog o tej samej nazwie pomiędzy istnieje () i mkdir (). W rzeczywistości, patrząc na to teraz, myślę, że mój test istnieje () może być trochę głupi.
Keith,
Keith: UUID jest bezpieczny lub nie jest w tym przypadku kluczowy. Wystarczy, że informacja o nazwie, do której pytałeś, „wycieka”. Załóżmy na przykład, że tworzony plik znajduje się w systemie plików NFS, a atakujący może (pasywnie) nasłuchiwać pakietów. Lub losowy stan generatora został wyciekły. W moim komentarzu powiedziałem, że twoje rozwiązanie jest równie niepewne jak zaakceptowana odpowiedź, ale to nie jest sprawiedliwe: zaakceptowane jest trywialne do pokonania za pomocą inotify, a to jest znacznie trudniejsze do pokonania. Niemniej jednak w niektórych scenariuszach jest to z pewnością możliwe.
zbyszek
2
Zastanawiałem się nad tym i wdrożyłem rozwiązanie wykorzystujące losowe UUID nieco takie jak ten. Nie ma sprawdzania istnienia, tylko jedna próba utworzenia - silne RNG stosowane przez metodę randomUUID praktycznie gwarantuje brak kolizji (może być używane do generowania kluczy podstawowych w tabelach DB, zrobiłem to sam i nigdy nie znałem kolizji), więc jestem całkiem pewny. Jeśli ktoś nie jest pewien, sprawdź stackoverflow.com/questions/2513573/...
brabster
Jeśli spojrzysz na implementację Javy, po prostu generują losowe nazwy, dopóki nie nastąpi kolizja. Ich maksymalne próby są nieskończone. Gdyby więc ktoś złośliwy odgadł nazwę twojego pliku / katalogu, zapętlałby się na zawsze. Oto link do źródła: hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/9fb81d7a2f16/src/share/… Myślałem, że może jakoś zablokować system plików, aby mógł atomowo wygenerować unikalną nazwę i utwórz katalog, ale myślę, że nie robi tego zgodnie z kodem źródłowym.
dosentmatter
5

Cóż, „createTempFile” faktycznie tworzy plik. Dlaczego więc najpierw go nie usunąć, a następnie wykonać na nim mkdir?

Paul Tomblin
źródło
1
Zawsze powinieneś sprawdzić wartość zwracaną dla mkdir (). Jeśli jest to fałsz, oznacza to, że katalog już istniał. Może to powodować problemy z bezpieczeństwem, więc zastanów się, czy powinno to spowodować błąd w Twojej aplikacji, czy nie.
Sarel Botha
1
Zobacz notatkę o stanie wyścigu w drugiej odpowiedzi.
Volker Stolz
To mi się podoba, z wyjątkiem wyścigu
Martin Wickman,
4

Ten kod powinien działać dość dobrze:

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;
}
matowy b
źródło
3
Co jeśli katalog już istnieje i nie masz do niego dostępu do odczytu / zapisu lub co jeśli jest to zwykły plik? Tam też masz warunki wyścigowe.
Jeremy Huiskamp
2
Ponadto deleteOnExit nie usunie niepustych katalogów.
Trenton,
3

Jak omówiono w tym RFE i jego komentarzach, możesz zadzwonić jako tempDir.delete()pierwszy. Lub możesz użyć System.getProperty("java.io.tmpdir")i utworzyć tam katalog. Tak czy inaczej, pamiętaj, aby zadzwonić tempDir.deleteOnExit(), w przeciwnym razie plik nie zostanie usunięty po zakończeniu.

Michael Myers
źródło
Czy ta właściwość nie nazywa się „java.io.tmpdir”, a nie „... temp”? Zobacz java.sun.com/j2se/1.4.2/docs/api/java/io/File.html
Andrew Swan,
Właśnie. Powinienem był zweryfikować przed powtórzeniem tego, co przeczytałem.
Michael Myers
Plik java.io.tmpdir jest udostępniony, więc musisz zrobić wszystkie zwykłe voodoo, aby uniknąć nadepnięcia komuś na palce.
Thorbjørn Ravn Andersen
3

Na zakończenie jest to kod z biblioteki Google guava. To nie jest mój kod, ale myślę, że warto go pokazać tutaj w tym wątku.

  /** 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)
            + ')');
  }
Arne
źródło
2

Mam ten sam problem, więc jest to kolejna odpowiedź dla zainteresowanych i jest podobna do jednej z powyższych:

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

W przypadku mojej aplikacji zdecydowałem, że dodam opcję wyczyszczenia temp przy wyjściu, więc dodałem hak zamykający:

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());
                    }
                }
            }
        }
    });

Metoda usuwa wszystkie podkatalogi i pliki przed usunięciem temp , bez użycia stosu wywołań (który jest całkowicie opcjonalny i w tym momencie można to zrobić z rekurencją), ale chcę być po bezpiecznej stronie.

niedorzeczny
źródło
2

Jak widać w innych odpowiedziach, nie pojawiło się żadne standardowe podejście. Dlatego wspomniałeś już o Apache Commons, proponuję następujące podejście przy użyciu FileUtils z Apache Commons IO :

/**
 * 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;

}

Jest to preferowane, ponieważ apache używa biblioteki, która jest jak najbliżej zadanego „standardu” i działa zarówno z JDK 7, jak i starszymi wersjami. Zwraca również „starą” instancję pliku (która jest oparta na strumieniu), a nie „nową” instancję ścieżki (która jest oparta na buforze i byłaby wynikiem metody getTemporaryDirectory () JDK7) -> Dlatego zwraca to, czego potrzebuje większość ludzi, gdy chcą utworzyć katalog tymczasowy.

Carsten Engelke
źródło
1

Lubię wiele prób stworzenia unikalnej nazwy, ale nawet to rozwiązanie nie wyklucza warunków wyścigu. Kolejny proces może wślizgnąć się po teście exists()i if(newTempDir.mkdirs())wywołaniu metody. Nie mam pojęcia, jak całkowicie uczynić to bezpiecznym bez uciekania się do natywnego kodu, który, jak przypuszczam, jest ukryty w środku File.createTempFile().

Chris Lott
źródło
1

Przed wersją Java 7 można również:

File folder = File.createTempFile("testFileUtils", ""); // no suffix
folder.delete();
folder.mkdirs();
folder.deleteOnExit();
geri
źródło
1
Niezły kod. Niestety „deleteOnExit ()” nie będzie działać, ponieważ Java nie może usunąć całego folderu na raz. Musisz usunąć wszystkie pliki rekurencyjnie: /
Adam Taras
1

Spróbuj tego małego przykładu:

Kod:

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


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

Dane wyjściowe konsoli na komputerze z systemem Windows:
C: \ Users \ nazwa_użytkownika \ AppData \ Local \ Temp \ tmpDir2908538301081367877

Komentarz:
Files.createTempDirectory generuje unikatowy identyfikator atomatycznie - 2908538301081367877.

Uwaga:
Przeczytaj rekursywnie następujące katalogi:
Usuń katalogi rekurencyjnie w Javie

DigitShifter
źródło
0

Używanie File#createTempFilei deletedo tworzenia unikalnej nazwy katalogu wydaje się być w porządku. Należy dodać a, ShutdownHookaby usunąć katalog (rekurencyjnie) podczas zamykania JVM.

ordnungswidrig
źródło
Hak zamykający jest uciążliwy. Czy plik # deleteOnExit również nie działałby?
Daniel Hiller
2
#deleteOnExit nie działało dla mnie - wierzę, że nie usunie niepustych katalogów.
muriloq,
Zaimplementowałem szybki test działający z Javą 8, ale folder tymczasowy nie został usunięty, patrz pastebin.com/mjgG70KG
geri