Як створити тимчасовий каталог / папку на Java?


364

Чи існує стандартний і надійний спосіб створення тимчасового каталогу всередині програми Java? У базі випусків Java є запис , який містить трохи коду у коментарях, але мені цікаво, чи є стандартне рішення, яке можна знайти в одній із звичайних бібліотек (Apache Commons тощо)?

Відповіді:


390

Якщо ви використовуєте 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), якщо хочете.


12
Це небезпечно. Ява, як відомо, не видаляє файли відразу, тому mkdir може вийти з ладу
Деміург

4
@Demiurg Єдиний випадок, коли файл не видаляється негайно, - це Windows, коли файл уже відкритий (наприклад, його може відкрити сканер вірусів). У вас є інша документація, щоб показати інакше (мені цікаво таких речей :-)? Якщо це трапляється регулярно, то вищевказаний код не працюватиме, якщо він рідкісний, тоді блокування виклику до вищевказаного коду, поки не відбудеться видалення (або не буде досягнуто декількох максимальних спроб).
TofuBeer

6
@Demiurg Java, як відомо, не видаляє файли відразу. Це правда, навіть якщо ви не відкриєте її. Отже, більш безпечний спосіб temp.delete(); temp = new File(temp.getPath + ".d"); temp.mkdir(); ..., temp.delete();.
Xiè Jìléi

102
Цей код має перегони між delete()та mkdir(): Зловмисний процес може тим часом створити цільовий каталог (взявши назву нещодавно створеного файлу). Шукайте Files.createTempDir()альтернативу.
Йоахім Зауер

11
Мені подобається ! щоб виділитися, занадто легко його не помітити. Я читаю багато коду, написаного студентами ... якщо (! Я) досить поширений, щоб дратувати :-)
TofuBeer

182

У бібліотеці Google Guava є багато корисних утиліт. Однією з приміток тут є клас Files . У ньому є маса корисних методів, серед яких:

File myTempDir = Files.createTempDir();

Це робить саме те, що ви просили в одному рядку. Якщо ви прочитаєте тут документацію, то побачите, що запропонована адаптація, File.createTempFile("install", "dir")як правило, вводить вразливості безпеки.


Цікаво, до якої вразливості ви посилаєтесь. Цей підхід не створює умови перегонів, оскільки File.mkdir () передбачається, що не вдасться, якщо такий каталог вже існує (створений зловмисником). Я не думаю, що цей дзвінок також відбуватиметься через зловмисні посилання. Чи можете ви пояснити, що ви мали на увазі?
abb

3
@abb: Я не знаю подробиць стану гонки, які згадуються в документації Guava. Я підозрюю, що документація є правильною, враховуючи, що вона конкретно викликає проблему.
Шпина

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

8
Доступний з JUnit 4.7
Eduard Wirch

Не працює в JUnit 4.8.2 на Windows 7! (Випуск дозволів)
виняток

2
@CraigRinger: Чому на це нерозумно покладатися?
Адам Паркін

2
@AdamParkin Чесно кажучи, я вже не пам'ятаю. Пояснення не вдалося!
Крейг Рінгер

1
Основна перевага цього підходу полягає в тому, що каталог керується JUnit (створений перед тестом та видалений рекурсивно після тесту). І це працює. Якщо ви отримали "temp dir не створено", це може бути тому, що ви забули @Rule або поле в не загальнодоступному.
Богдан Кальмак

42

Наївно написаний код для вирішення цієї проблеми страждає від перегонових умов, включаючи тут кілька відповідей. Історично ви могли добре подумати над умовами перегонів і записати їх самостійно, або можете скористатися сторонніми бібліотеками, такими як Гуава Google (як підказує відповідь Спіна.) Або ви можете написати код баггі.

Але щодо JDK 7, є хороші новини! Сама стандартна бібліотека Java надає належно працююче (не раціональне) рішення цієї проблеми. Ви хочете java.nio.file.Files # createTempDirectory () . З документації :

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

Створює новий каталог у вказаному каталозі, використовуючи даний префікс для створення його імені. Отриманий шлях пов'язаний з тією ж файловою системою, що і даний каталог.

Деталі про те, як побудовано ім'я каталогу, залежать від реалізації і тому не задаються. Де можливо, префікс використовується для побудови імен кандидатів.

Це ефективно вирішує бентежно старий звіт про помилки в трекері про помилки Sun, який попросив саме таку функцію.


35

Це вихідний код файлів бібліотеки Guava Files.createTempDir (). Це ніде не так складно, як можна подумати:

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 - зло" для отримання додаткової інформації, але суть проблеми полягає в наступному:

  1. deleteOnExit() видаляє лише для звичайного відключення JVM, а не збоїв і не знищує процес JVM.

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

  3. Найбільше зло з усіх - deleteOnExit()споживає пам'ять для кожного запису тимчасового файлу. Якщо ваш процес працює місяцями або створюється багато тимчасових файлів за короткий час, ви споживаєте пам’ять і ніколи не випускаєте її, поки JVM не вимкнеться.


1
У нас є JVM, де файли класів і jar отримують приховані файли під створеними JVM, і ця додаткова інформація потребує досить тривалого часу, щоб видалити. Роблячи гарячі повторні розробки веб-контейнерів, що вибухають ВІЙНИ, JVM може буквально зайняти кілька хвилин, щоб очистити їх після закінчення, але перед тим, як вийти, коли пробіг протягом декількох годин.
Thorbjørn Ravn Andersen

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
Це небезпечно. Дивіться коментар Йоахіма Зауера в першому (однаково небезпечному) варіанті. Правильний спосіб перевірити наявність файлу чи dir та, ніж схопити ім’я файлу, атомічно, - створити файл чи dir.
zbyszek

1
@zbyszek javadocs кажуть, що "UUID генерується за допомогою криптографічно сильного генератора псевдовипадкових чисел". З огляду на те, як шкідливий процес створює реж. З однаковою назвою між існує () та mkdirs (). Насправді, дивлячись на це зараз, я думаю, що мій тест існує () може бути трохи дурним.
Кіт

Кіт: Захищеність UUID чи не є вирішальним у цьому випадку. Досить інформації про ім’я, яке ви запитували, щоб якось "просочитися". Наприклад, скажімо, що створений файл знаходиться у файловій системі NFS, і зловмисник може слухати (пасивно) пакети. Або випадковий стан генератора просочився. У своєму коментарі я сказав, що ваше рішення не менш небезпечне, як прийнята відповідь, але це не справедливо: прийняте - тривіальне, щоб перемогти інотифікувати, а цього набагато важче перемогти. Тим не менш, у деяких сценаріях це, безумовно, можливо.
zbyszek

2
Я мав таку ж думку і реалізував рішення, використовуючи випадкові біти UUID, як цей. Немає перевірки на наявність, лише одна спроба створити - сильний RNG, що використовується методом randomUUID, в значній мірі гарантує відсутність зіткнень (можна використовувати для генерації первинних ключів у таблицях БД, робив це сам і ніколи не знав зіткнення), тому досить впевнено. Якщо хто -то не впевнений, перевірити stackoverflow.com/questions/2513573 / ...
brabster

Якщо ви подивитеся на реалізацію Java, вони просто генерують випадкові імена, поки не буде зіткнення. Їх максимум спроб нескінченний. Тож якщо хтось зловмисник мав би вгадувати ім’я вашого файлу / каталогу, він зациклюється назавжди. Ось посилання на джерело: hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/9fb81d7a2f16/src/share/… Я думав, що це може якось заблокувати файлову систему, щоб вона могла атомно генерувати унікальне ім'я та створити каталог, але я думаю, що це не робиться відповідно до вихідного коду.
дозаматмат

5

Ну, "createTempFile" фактично створює файл. То чому б не просто видалити його спочатку, а потім зробити mkdir на ньому?


1
Ви завжди повинні перевіряти значення повернення для mkdir (). Якщо це неправда, то це означає, що каталог вже існував. Це може спричинити проблеми із безпекою, тому подумайте, чи повинно це спричинити помилку у вашій програмі чи ні.
Сарел Бота

1
Дивіться примітку про стан гонки в іншій відповіді.
Волкер Столц

Це мені подобається, забороняючи гонку
Мартін Вікман,

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

Як було обговорено в цій радіомовлення та її коментарях, ви можете зателефонувати 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 надається спільним, тому вам потрібно виконати всі звичні вуду, щоб не наступати на когось інші пальці.
Thorbjørn Ravn Andersen

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

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

Спосіб видаляє всі підкаталоги та файли перед тим, як видалити темп , не використовуючи callstack (що зовсім необов’язково. Ви можете зробити це з рекурсією в цей момент), але я хочу бути в безпечній частині.


2

Як видно з інших відповідей, стандартного підходу не склалося. Отже, ви вже згадали про Apache Commons, пропоную наступний підхід, використовуючи FileUtils з IO Apache Commons :

/**
 * 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, так і з більш старими версіями. Це також повертає "старий" екземпляр файлу (який базується на потоці), а не "новий" екземпляр Path (який заснований на буфері і був би результатом методу getTemvremeDirectory () 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 \ userName \ AppData \ Local \ Temp \ tmpDir2908538301081367877

Коментар:
Files.createTempDirectory генерує унікальний ідентифікатор атомно - 2908538301081367877.

Примітка:
Читайте наступне для рекурсивного видалення каталогів:
Видалення каталогів рекурсивно в Java


0

Використання File#createTempFileта deleteстворення унікального імені для каталогу здається нормальним. Ви повинні додати ShutdownHookпапку для видалення каталогу (рекурсивно) після вимкнення JVM.


Гак відключення громіздкий. Чи не працює також файл # deleteOnExit?
Даніель Гіллер

2
#deleteOnExit не працював для мене - я вважаю, що він не видалить непорожні каталоги.
muriloq

Я реалізував швидкий тест під керуванням Java 8, але папку temp не видалено, див. Pastebin.com/mjgG70KG
гері
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.