Как создать временный каталог / папку в Java?

365

Существует ли стандартный и надежный способ создания временного каталога в приложении Java? В базе данных выпусков Java есть запись, в которой есть небольшой код в комментариях, но мне интересно, есть ли стандартное решение в одной из обычных библиотек (Apache Commons и т. Д.)?

Питер Беккер
источник

Ответы:

391

Если вы используете JDK 7, используйте новый класс Files.createTempDirectory для создания временного каталога.

Path tempDirWithPrefix = Files.createTempDirectory(prefix);

Перед JDK 7 это должно сделать это:

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

Вы можете сделать лучшие исключения (подкласс IOException), если хотите.

TofuBeer
источник
12
Это опасно Известно, что Java не удаляет файлы сразу, поэтому mkdir может иногда завершаться с ошибкой
Demiurg
4
@Demiurg Единственный случай, когда файл не удаляется немедленно, - в Windows, когда файл уже открыт (например, он может быть открыт антивирусным сканером). У вас есть другая документация, чтобы показать иначе (мне любопытно о таких вещах :-)? Если это происходит регулярно, то приведенный выше код не будет работать, если он случается редко, тогда будет работать прерывание вызова вышеуказанного кода до тех пор, пока не произойдет удаление (или не будет достигнуто максимальное количество попыток).
TofuBeer
6
@ Демиург Java, как известно, не удаляет файлы сразу. Это правда, даже если вы не откроете это. Итак, более безопасный способ temp.delete(); temp = new File(temp.getPath + ".d"); temp.mkdir(); ..., temp.delete();.
Xiè Jìléi
102
Этот код имеет условие состязания между delete()и mkdir(): В то же время злонамеренный процесс может создать целевой каталог (принимая имя недавно созданного файла). Смотрите Files.createTempDir()для альтернативы.
Иоахим Зауэр
11
Мне нравится ! выделяться, слишком легко пропустить это. Я прочитал много кода, написанного студентами ... если (! I) достаточно распространено, чтобы раздражать :-)
TofuBeer
182

В библиотеке Google Guava есть множество полезных утилит. Одним из примечаний здесь является класс Files . У него есть куча полезных методов, в том числе:

File myTempDir = Files.createTempDir();

Это делает именно то, что вы просили в одной строке. Если вы прочитаете здесь документацию, то увидите, что предлагаемая адаптация File.createTempFile("install", "dir")обычно представляет уязвимости безопасности.

Спина
источник
Интересно, на какую уязвимость вы ссылаетесь? Этот подход, по-видимому, не создает условия состязания, поскольку File.mkdir () может завершиться ошибкой, если такой каталог уже существует (созданный злоумышленником). Я не думаю, что этот звонок будет следовать через вредоносные символические ссылки. Не могли бы вы уточнить, что вы имели в виду?
abb
3
@abb: я не знаю деталей состояния гонки, которые упоминаются в документации по гуаве. Я подозреваю, что документация является правильной, учитывая, что она конкретно вызывает проблему.
Спина
1
@abb Ты прав. Пока проверяется возврат mkdir (), он будет безопасным. Код Spina указывает на использование этого метода mkdir (). grepcode.com/file/repo1.maven.org/maven2/com.google.guava/guava/… . Это только потенциальная проблема в системах Unix при использовании каталога / tmp, потому что в нем включен липкий бит.
Сарел Бота
@SarelBotha спасибо за заполнение пробела здесь. Я довольно долго об этом думал.
Спина
168

Если вам нужен временный каталог для тестирования и вы используете jUnit, @Ruleвместе с TemporaryFolderрешает вашу проблему:

@Rule
public TemporaryFolder folder = new TemporaryFolder();

Из документации :

Правило TemporaryFolder позволяет создавать файлы и папки, которые гарантированно удаляются после завершения метода тестирования (независимо от того, пройден он или нет)


Обновить:

Если вы используете JUnit Jupiter (версия 5.1.1 или выше), у вас есть возможность использовать JUnit Pioneer, который является пакетом расширений JUnit 5.

Скопировано из проектной документации :

Например, следующий тест регистрирует расширение для одного метода теста, создает и записывает файл во временный каталог и проверяет его содержимое.

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

Больше информации в JavaDoc и JavaDoc 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>

Обновление 2:

@TempDir аннотаций была добавлена в JUnit Jupiter 5.4.0 выпуска в качестве экспериментальной функции. Пример, скопированный из Руководства пользователя 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
источник
8
Доступно с JUnit 4.7
Эдуард Вирч,
Не работает в JUnit 4.8.2 на Windows 7! (Проблема с разрешениями)
исключение
2
@CraigRinger: Почему неразумно полагаться на это?
Адам Паркин
2
@AdamParkin Честно говоря, я не помню больше. Объяснение не удалось!
Крейг Рингер,
1
Основным преимуществом этого подхода является то, что каталог управляется JUnit (создается до теста и рекурсивно удаляется после теста). И это работает. Если вы получили «временный каталог еще не создан», это может быть потому, что вы забыли @Rule или поле недоступно.
Богдан Калмак
42

Наивно написанный код для решения этой проблемы страдает от условий гонки, в том числе несколько ответов здесь. Исторически сложилось так, что вы могли тщательно обдумать условия гонки и написать ее самостоятельно, или вы могли бы использовать стороннюю библиотеку, такую ​​как Google Guava (как подсказал ответ Спины). Или вы могли бы написать глючный код.

Но с JDK 7 есть хорошие новости! Сама стандартная библиотека Java теперь предоставляет правильно работающее (нерасовое) решение этой проблемы. Вы хотите java.nio.file.Files # createTempDirectory () . Из документации :

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

Создает новый каталог в указанном каталоге, используя данный префикс для генерации его имени. Результирующий путь связан с той же файловой системой, что и данный каталог.

Детали того, как создается имя каталога, зависят от реализации и поэтому не указаны. Где возможно, префикс используется для создания имен кандидатов.

Это эффективно разрешает неловко древний отчет об ошибках в трекере ошибок Sun, который запрашивал именно такую ​​функцию.

Грег Прайс
источник
35

Это исходный код для Files.createTempDir () библиотеки Guava. Это нигде не так сложно, как вы думаете

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

По умолчанию:

private static final int TEMP_DIR_ATTEMPTS = 10000;

Посмотреть здесь

Андрес Киевский
источник
28

Не используйте, deleteOnExit()даже если вы явно удалите его позже.

Google «deleteonexit is evil» для получения дополнительной информации, но суть проблемы заключается в следующем:

  1. deleteOnExit() удаляет только для нормальных завершений JVM, а не для сбоев или уничтожения процесса JVM.

  2. deleteOnExit() удаляет только при завершении работы JVM - не подходит для длительных процессов сервера, потому что:

  3. Самое злое из всех - deleteOnExit()потребляет память для каждой записи временного файла. Если ваш процесс выполняется месяцами или создает много временных файлов за короткое время, вы расходуете память и никогда не освобождаете ее, пока JVM не отключится.

Разработчик чувак
источник
1
У нас есть JVM, в которой файлы классов и jar-файлов получают скрытые файлы, созданные под ним, и эта дополнительная информация занимает много времени для удаления. При горячем повторном развертывании веб-контейнеров с разрывами WAR, JVM может буквально занимать минуты для очистки после завершения, но перед выходом, если он работал в течение нескольких часов.
Турбьёрн Равн Андерсен
20

Начиная с Java 1.7 createTempDirectory(prefix, attrs)и createTempDirectory(dir, prefix, attrs)включены вjava.nio.file.Files

Пример: File tempDir = Files.createTempDirectory("foobar").toFile();

ищущий
источник
14

Вот что я решил сделать для моего собственного кода:

/**
 * 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
Это небезопасно. См. Комментарий Йоахима Зауэра в первом (столь же небезопасном) варианте. Правильный способ проверить наличие файла или директории, а затем получить имя файла, атомарно, путем создания файла или директории.
zbyszek
1
@zbyszek javadocs говорят: «UUID генерируется с использованием криптографически сильного генератора псевдослучайных чисел». Учитывая то, как злонамеренный процесс создает dir с тем же именем между there () и mkdirs (). На самом деле, глядя на это сейчас, я думаю, что мой тест exist () может быть немного глупым.
Кит
Кит: UUID быть безопасным или нет не имеет решающего значения в этом случае. Этого достаточно для того, чтобы информация об имени, которое вы запрашивали, каким-то образом «просочилась». Например, предположим, что создаваемый файл находится в файловой системе NFS, и злоумышленник может прослушивать (пассивно) пакеты. Или случайный генератор был утечкой. В своем комментарии я сказал, что ваше решение в равной степени небезопасно, как и принятый ответ, но это несправедливо: принятый вариант тривиален, чтобы победить с помощью inotify, а этот гораздо сложнее победить. Тем не менее, в некоторых сценариях это, безусловно, возможно.
zbyszek
2
У меня была такая же мысль, и я реализовал решение с использованием случайных UUID, таких как этот. Никакой проверки не существует, только одна попытка создать - сильный RNG, используемый методом randomUUID, в значительной степени гарантирует отсутствие коллизий (может использоваться для генерации первичных ключей в таблицах БД, делал это сам и никогда не знал коллизий), поэтому довольно уверенно. Если кто-то не уверен, проверьте stackoverflow.com/questions/2513573/…
brabster
Если вы посмотрите на реализацию Java, они просто генерируют случайные имена, пока не произойдет столкновение. Их максимальные попытки бесконечны. Так что, если кто-то злоумышленник продолжит угадывать имя вашего файла / каталога, он будет зацикливаться вечно. Вот ссылка на источник: hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/9fb81d7a2f16/src/share/… Я думал, что он может каким-то образом заблокировать файловую систему, чтобы он мог атомарно сгенерировать уникальное имя и создать каталог, но я думаю, что он не делает этого в соответствии с исходным кодом.
dosentmatter
5

Ну, «createTempFile» фактически создает файл. Так почему бы просто не удалить его сначала, а затем сделать на нем mkdir?

Пол Томблин
источник
1
Вы всегда должны проверять возвращаемое значение для mkdir (). Если это неверно, значит, каталог уже существует. Это может вызвать проблемы с безопасностью, поэтому подумайте, должно ли это вызвать ошибку в вашем приложении или нет.
Сарел Бота
1
Смотрите примечание о состоянии гонки в другом ответе.
Volker Stolz
Это мне нравится, за исключением гонки
Мартин Викман
4

Этот код должен работать достаточно хорошо:

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
Что если каталог уже существует и у вас нет прав на чтение / запись или что если это обычный файл? У вас также есть условия гонки там.
Джереми Хуискамп
2
Кроме того, deleteOnExit не будет удалять непустые каталоги.
Трентон
3

Как уже говорилось в этом RFE и его комментариях, вы можете позвонить tempDir.delete()первым. Или вы можете использовать System.getProperty("java.io.tmpdir")и создать каталог там. В любом случае, вы должны помнить, чтобы позвонить tempDir.deleteOnExit(), или файл не будет удален после того, как вы закончите.

Майкл Майерс
источник
Разве это свойство не называется "java.io.tmpdir", а не "... temp"? См. Java.sun.com/j2se/1.4.2/docs/api/java/io/File.html
Эндрю Свон
Именно так. Я должен был проверить, прежде чем повторять то, что я прочитал.
Майкл Майерс
Java.io.tmpdir является общим, поэтому вам нужно выполнять все обычные вуду, чтобы не наступать кому-то еще на пальцы ног.
Турбьерн Равн Андерсен
3

Просто для завершения, это код из библиотеки Google Guava. Это не мой код, но я думаю, что стоит показать его здесь, в этой теме.

  /** 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
источник
2

У меня та же проблема, так что это просто еще один ответ для тех, кто заинтересован, и он похож на один из вышеперечисленных:

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

И для моего приложения я решил добавить опцию для очистки временного интервала при выходе, поэтому я добавил в ловушку выключения:

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

Метод удаляет все подкаталоги и файлы перед удалением temp , без использования callstack (что совершенно необязательно, и вы можете сделать это с рекурсией на этом этапе), но я хочу быть на безопасной стороне.

immoteous
источник
2

Как видно из других ответов, стандартного подхода не возникло. Следовательно, вы уже упомянули Apache Commons, я предлагаю следующий подход с использованием FileUtils из 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;

}

Это предпочтительнее, поскольку apache объединяет библиотеку, которая наиболее близка к запрашиваемому «стандарту» и работает как с JDK 7, так и с более старыми версиями. Это также возвращает «старый» экземпляр File (основанный на потоке), а не «новый» экземпляр Path (основанный на буфере и являющийся результатом метода getDeventDirectory ()) JDK7 -> Поэтому он возвращает то, что нужно большинству людей, когда они хотят создать временный каталог.

Карстен Энгельке
источник
1

Мне нравятся многочисленные попытки создания уникального имени, но даже это решение не исключает условия гонки. Другой процесс может проскочить после проверки exists()и if(newTempDir.mkdirs())вызова метода. Я понятия не имею, как полностью сделать это безопасным, не прибегая к нативному коду, который, как я предполагаю, является тем, что скрыто внутри File.createTempFile().

Крис Лотт
источник
1

До Java 7 вы также могли:

File folder = File.createTempFile("testFileUtils", ""); // no suffix
folder.delete();
folder.mkdirs();
folder.deleteOnExit();
гери
источник
1
Хороший код Но, к сожалению, «deleteOnExit ()» не будет работать, так как Java не может удалить всю папку сразу. Вы должны удалить все файлы рекурсивно: /
Адам Тарас
1

Попробуйте этот небольшой пример:

Код:

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


Импорт:
java.io.IOException
java.nio.file.Files
java.nio.file.Path

Вывод на консоль на компьютере Windows:
C: \ Users \ имя_пользователя \ AppData \ Local \ Temp \ tmpDir2908538301081367877

Комментарий:
Files.createTempDirectory автоматически генерирует уникальный идентификатор - 2908538301081367877.

Примечание.
Прочитайте следующее для рекурсивного удаления каталогов: рекурсивное
удаление каталогов в Java

DigitShifter
источник
0

Использование File#createTempFileи deleteдля создания уникального имени для каталога кажется нормальным. Вы должны добавить, ShutdownHookчтобы (рекурсивно) удалить каталог при завершении работы JVM.

ordnungswidrig
источник
Отключающий крюк громоздок. Файл # deleteOnExit не будет работать?
Даниэль Хиллер
2
#deleteOnExit не работал для меня - я считаю, что он не удалит непустые каталоги.
Muriloq
Я реализовал быстрый тест, работающий с Java 8, но временная папка не была удалена, см. Pastebin.com/mjgG70KG
geri