Bagaimana cara membuat direktori / folder sementara di Java?

364

Apakah ada cara standar dan dapat diandalkan untuk membuat direktori sementara di dalam aplikasi Java? Ada entri dalam database masalah Java , yang memiliki sedikit kode dalam komentar, tapi saya ingin tahu apakah ada solusi standar yang dapat ditemukan di salah satu perpustakaan biasa (Apache Commons dll)?

Peter Becker
sumber

Jawaban:

390

Jika Anda menggunakan JDK 7 gunakan kelas Files.createTempDirectory baru untuk membuat direktori sementara.

Path tempDirWithPrefix = Files.createTempDirectory(prefix);

Sebelum JDK 7 ini harus dilakukan:

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

Anda bisa membuat pengecualian yang lebih baik (subkelas IOException) jika Anda mau.

TofuBeer
sumber
12
Ini berbahaya. Java diketahui tidak segera menghapus file, jadi mkdir terkadang gagal
Demiurg
4
@Demiurg Satu-satunya kasus file yang tidak segera dihapus adalah pada Windows ketika file sudah terbuka (itu bisa dibuka oleh pemindai virus misalnya). Apakah Anda memiliki dokumentasi lain untuk ditampilkan sebaliknya (Saya ingin tahu tentang hal-hal seperti itu :-)? Jika itu terjadi secara teratur maka kode di atas tidak akan berfungsi, jika jarang maka lipping panggilan ke kode di atas sampai penghapusan terjadi (atau beberapa upaya maks tercapai) akan bekerja.
TofuBeer
6
@Demiurg Java dikenal tidak segera menghapus file. Itu benar, bahkan jika Anda tidak membukanya. Jadi, cara yang lebih aman adalah temp.delete(); temp = new File(temp.getPath + ".d"); temp.mkdir(); ..., temp.delete();.
Xiè Jìléi
102
Kode ini memiliki kondisi balapan antara delete()dan mkdir(): Sementara itu, proses jahat dapat membuat direktori target (mengambil nama file yang baru dibuat). Lihat Files.createTempDir()alternatifnya.
Joachim Sauer
11
Saya suka ! untuk menonjol, terlalu mudah untuk dilewatkan. Saya membaca banyak kode yang ditulis oleh siswa ... jika (! I) cukup umum untuk menjadi menyebalkan :-)
TofuBeer
182

Pustaka Google Guava memiliki banyak utilitas bermanfaat. Salah satu catatan di sini adalah kelas Files . Ini memiliki banyak metode yang berguna termasuk:

File myTempDir = Files.createTempDir();

Ini melakukan persis apa yang Anda minta dalam satu baris. Jika Anda membaca dokumentasi di sini Anda akan melihat bahwa adaptasi yang diusulkan File.createTempFile("install", "dir")biasanya menimbulkan kerentanan keamanan.

Spina
sumber
Saya ingin tahu kerentanan apa yang Anda rujuk. Pendekatan ini tampaknya tidak menciptakan kondisi balapan karena File.mkdir () dianggap gagal jika direktori tersebut sudah ada (dibuat oleh penyerang). Saya tidak berpikir panggilan ini akan mengikuti symlink jahat juga. Bisakah Anda mengklarifikasi apa yang Anda maksud?
abb
3
@abb: Saya tidak tahu detail kondisi lomba yang disebutkan dalam dokumentasi Guava. Saya menduga bahwa dokumentasinya benar mengingat secara spesifik menyebutkan masalahnya.
Spina
1
@abb Anda benar. Selama kembalinya mkdir () dicentang maka itu akan aman. Kode Spina menunjuk untuk menggunakan metode mkdir () ini. grepcode.com/file/repo1.maven.org/maven2/com.google.guava/guava/… . Ini hanya masalah potensial pada sistem Unix ketika menggunakan direktori / tmp karena itu memiliki bit sticky diaktifkan.
Sarel Botha
@SarelBotha terima kasih telah mengisi bagian yang kosong di sini. Aku sudah bertanya-tanya tentang hal ini selama beberapa waktu.
Spina
168

Jika Anda memerlukan direktori sementara untuk pengujian dan Anda menggunakan jUnit, @Rulebersama dengan TemporaryFoldermemecahkan masalah Anda:

@Rule
public TemporaryFolder folder = new TemporaryFolder();

Dari dokumentasi :

Aturan TemporaryFolder memungkinkan pembuatan file dan folder yang dijamin akan dihapus ketika metode pengujian selesai (apakah itu lolos atau gagal)


Memperbarui:

Jika Anda menggunakan JUnit Jupiter (versi 5.1.1 atau lebih tinggi), Anda memiliki opsi untuk menggunakan JUnit Pioneer yang merupakan Paket Ekstensi JUnit 5.

Disalin dari dokumentasi proyek :

Misalnya, pengujian berikut mendaftarkan ekstensi untuk metode pengujian tunggal, membuat dan menulis file ke direktori sementara dan memeriksa kontennya.

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

Info selengkapnya di JavaDoc dan JavaDoc dari 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>

Pembaruan 2:

The @TempDir penjelasan ditambahkan ke JUnit Jupiter 5.4.0 rilis sebagai fitur eksperimental. Contoh disalin dari Panduan Pengguna 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
sumber
8
Tersedia sejak JUnit 4.7
Eduard Wirch
Tidak bekerja di JUnit 4.8.2 di Windows 7! (Masalah perizinan)
pengecualian
2
@CraigRinger: Mengapa tidak bijaksana mengandalkan ini?
Adam Parkin
2
@ AdamParkin Jujur, saya tidak ingat lagi. Penjelasan gagal!
Craig Ringer
1
Manfaat utama dari pendekatan ini adalah bahwa direktori dikelola oleh JUnit (dibuat sebelum pengujian dan dihapus secara rekursif setelah pengujian). Dan itu berhasil. Jika Anda mendapatkan "temp dir belum dibuat", itu mungkin karena Anda lupa @Rule atau bidang tidak publik.
Bogdan Calmac
42

Kode yang ditulis secara naif untuk mengatasi masalah ini menderita kondisi lomba, termasuk beberapa jawaban di sini. Secara historis Anda dapat berpikir dengan hati-hati tentang kondisi balapan dan menulisnya sendiri, atau Anda dapat menggunakan perpustakaan pihak ketiga seperti Google Guava (seperti yang disarankan oleh Spina.) Atau Anda dapat menulis kode kereta.

Tetapi pada JDK 7, ada kabar baik! Pustaka standar Java sendiri sekarang menyediakan solusi yang berfungsi dengan baik (non-bersemangat) untuk masalah ini. Anda ingin java.nio.file.Files # createTempDirectory () . Dari dokumentasi :

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

Membuat direktori baru di direktori yang ditentukan, menggunakan awalan yang diberikan untuk menghasilkan namanya. Path yang dihasilkan dikaitkan dengan FileSystem yang sama dengan direktori yang diberikan.

Rincian tentang bagaimana nama direktori dibangun tergantung pada implementasi dan karenanya tidak ditentukan. Jika memungkinkan awalan digunakan untuk membuat nama kandidat.

Ini secara efektif menyelesaikan laporan bug kuno yang memalukan di pelacak bug Sun yang meminta fungsi seperti itu.

Greg Price
sumber
35

Ini adalah kode sumber ke Files.createTempDir () perpustakaan perpustakaan. Ini tidak serumit yang Anda kira:

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

Secara default:

private static final int TEMP_DIR_ATTEMPTS = 10000;

Lihat disini

Andres Kievsky
sumber
28

Jangan gunakan deleteOnExit()bahkan jika Anda secara eksplisit menghapusnya nanti.

Google 'deleteonexit is evil' untuk info lebih lanjut, tetapi inti masalahnya adalah:

  1. deleteOnExit() hanya menghapus untuk shutdown JVM normal, tidak crash atau membunuh proses JVM.

  2. deleteOnExit() hanya menghapus JVM shutdown - tidak bagus untuk proses server yang berjalan lama karena:

  3. Yang paling jahat dari semuanya - deleteOnExit()menghabiskan memori untuk setiap entri file temp. Jika proses Anda berjalan selama berbulan-bulan, atau membuat banyak file temp dalam waktu singkat, Anda menggunakan memori dan tidak pernah melepaskannya sampai JVM dimatikan.

Pengembang Bung
sumber
1
Kami memiliki JVM tempat file kelas dan jar mendapatkan file tersembunyi di bawahnya yang dibuat oleh JVM, dan informasi tambahan ini butuh waktu cukup lama untuk dihapus. Saat melakukan redeploys panas pada wadah web yang meledak WAR, JVM dapat secara literal membutuhkan waktu beberapa menit untuk dibersihkan setelah selesai tetapi sebelum keluar saat berlari selama beberapa jam.
Thorbjørn Ravn Andersen
20

Pada Java 1.7 createTempDirectory(prefix, attrs)dan createTempDirectory(dir, prefix, attrs)termasuk dalamjava.nio.file.Files

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

pencari
sumber
14

Inilah yang saya putuskan untuk lakukan untuk kode saya sendiri:

/**
 * 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
sumber
2
Ini tidak aman. Lihat komentar oleh Joachim Sauer dalam opsi pertama (sama-sama tidak aman). Cara yang tepat untuk memeriksa keberadaan file atau dir dan daripada mengambil nama file, secara atom, adalah dengan membuat file atau dir.
zbyszek
1
@zbyszek javadocs mengatakan "UUID dibuat menggunakan generator nomor acak pseudo kriptografi yang kuat." Mengingat bahwa bagaimana proses jahat membuat dir dengan nama yang sama antara exist () dan mkdirs (). Sebenarnya melihat ini sekarang saya pikir ada () tes saya mungkin agak konyol.
Keith
Keith: UUID aman atau tidak tidak penting dalam kasus ini. Cukup untuk informasi tentang nama yang Anda tanyakan untuk "bocor". Sebagai contoh, katakanlah file yang sedang dibuat ada di sistem file NFS, dan penyerang dapat mendengarkan (secara pasif) paket. Atau keadaan generator acak telah bocor. Dalam komentar saya, saya mengatakan bahwa solusi Anda sama tidak amannya dengan jawaban yang diterima, tetapi ini tidak adil: yang diterima sepele untuk dikalahkan dengan inotify, dan yang ini jauh lebih sulit dikalahkan. Namun demikian, dalam beberapa skenario itu tentu mungkin.
zbyszek
2
Saya memiliki pemikiran yang sama dan mengimplementasikan solusi menggunakan bit UUID acak seperti ini. Tidak ada pemeriksaan untuk ada, hanya satu upaya untuk membuat - RNG kuat yang digunakan oleh metode randomUUID cukup banyak menjamin tidak ada tabrakan (dapat digunakan untuk menghasilkan kunci utama dalam tabel DB, melakukan ini sendiri dan tidak pernah tahu tabrakan), jadi cukup percaya diri. Jika ada yang tidak yakin, lihat stackoverflow.com/questions/2513573/…
brabster
Jika Anda melihat implementasi Java, mereka hanya menghasilkan nama acak sampai tidak ada tabrakan. Upaya maksimal mereka tidak terbatas. Jadi, jika seseorang jahat terus menebak nama file / direktori Anda, itu akan berulang selamanya. Berikut ini tautan ke sumber: hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/9fb81d7a2f16/src/share/... Saya berpikir bahwa itu bisa mengunci sistem file sehingga secara atomik dapat menghasilkan nama dan nama yang unik. buat direktori, tapi saya rasa itu tidak melakukan itu sesuai dengan kode sumber.
dosentmatter
5

Yah, "createTempFile" sebenarnya membuat file. Jadi mengapa tidak hapus saja dulu, lalu lakukan mkdir di atasnya?

Paul Tomblin
sumber
1
Anda harus selalu memeriksa nilai kembali untuk mkdir (). Jika itu salah maka itu berarti direktori sudah ada. Ini dapat menyebabkan masalah keamanan, jadi pertimbangkan apakah ini akan menimbulkan kesalahan dalam aplikasi Anda atau tidak.
Sarel Botha
1
Lihat catatan tentang kondisi balapan di jawaban lain.
Volker Stolz
Ini saya suka, kecuali balapan
Martin Wickman
4

Kode ini harus bekerja dengan cukup baik:

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;
}
matt b
sumber
3
Bagaimana jika direktori sudah ada dan Anda tidak memiliki akses baca / tulis ke sana atau bagaimana jika itu file biasa? Anda juga memiliki kondisi balapan di sana.
Jeremy Huiskamp
2
Juga, deleteOnExit tidak akan menghapus direktori yang tidak kosong.
Trenton
3

Seperti dibahas dalam RFE ini dan komentarnya, Anda dapat menelepon tempDir.delete()terlebih dahulu. Atau Anda bisa menggunakan System.getProperty("java.io.tmpdir")dan membuat direktori di sana. Bagaimanapun, Anda harus ingat untuk menelepon tempDir.deleteOnExit(), atau file tidak akan dihapus setelah Anda selesai.

Michael Myers
sumber
Bukankah properti ini disebut "java.io.tmpdir", bukan "... temp"? Lihat java.sun.com/j2se/1.4.2/docs/api/java/io/File.html
Andrew Swan
Kira-kira. Saya harus memverifikasi sebelum mengulangi apa yang saya baca.
Michael Myers
Java.io.tmpdir dibagikan sehingga Anda perlu melakukan semua voodoo yang biasa untuk menghindari menginjak jari kaki orang lain.
Thorbjørn Ravn Andersen
3

Hanya untuk penyelesaian, ini adalah kode dari perpustakaan google jambu. Ini bukan kode saya, tapi saya pikir sangat berharga untuk menunjukkannya di sini di utas ini.

  /** 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
sumber
2

Saya mendapat masalah yang sama jadi ini hanyalah jawaban lain bagi mereka yang tertarik, dan ini mirip dengan salah satu di atas:

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

Dan untuk aplikasi saya, saya memutuskan untuk menambahkan opsi untuk menghapus temp pada saat keluar jadi saya menambahkan di hook-down:

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

Metode menghapus semua subdir dan file sebelum menghapus temp , tanpa menggunakan callstack (yang benar-benar opsional dan Anda bisa melakukannya dengan rekursi pada titik ini), tetapi saya ingin berada di sisi yang aman.

immoteous
sumber
2

Seperti yang Anda lihat di jawaban lain, tidak ada pendekatan standar yang muncul. Karena itu Anda sudah menyebut Apache Commons, saya mengusulkan pendekatan berikut menggunakan FileUtils dari 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;

}

Ini lebih disukai karena apache commons perpustakaan yang datang paling dekat dengan "standar" yang diminta dan bekerja dengan JDK 7 dan versi yang lebih lama. Ini juga mengembalikan contoh File "lama" (yang berbasis aliran) dan bukan contoh Path "baru" (yang berbasis buffer dan akan menjadi hasil dari metode getTemporaryDirectory () JDK7) -> Karena itu ia mengembalikan apa yang dibutuhkan kebanyakan orang saat mereka ingin membuat direktori sementara.

Carsten Engelke
sumber
1

Saya suka beberapa upaya membuat nama yang unik tetapi bahkan solusi ini tidak mengesampingkan kondisi balapan. Proses lain dapat menyelinap masuk setelah tes untuk exists()dan if(newTempDir.mkdirs())metode doa Saya tidak tahu bagaimana membuat ini sepenuhnya aman tanpa menggunakan kode asli, yang saya kira adalah apa yang terkubur di dalamnya File.createTempFile().

Chris Lott
sumber
1

Sebelum Java 7 Anda juga bisa:

File folder = File.createTempFile("testFileUtils", ""); // no suffix
folder.delete();
folder.mkdirs();
folder.deleteOnExit();
Geri
sumber
1
Kode yang bagus Tetapi sayangnya "deleteOnExit ()" tidak akan berfungsi, karena Java tidak dapat menghapus seluruh folder sekaligus. Anda harus menghapus semua file secara rekursif: /
Adam Taras
1

Coba contoh kecil ini:

Kode:

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


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

Output konsol pada mesin Windows:
C: \ Users \ userName \ AppData \ Local \ Temp \ tmpDir2908538301081367877

Komentar:
Files.createTempDirectory menghasilkan ID unik secara otomatis - 2908538301081367877.

Catatan:
Baca berikut ini untuk menghapus direktori secara berulang :
Hapus direktori secara rekursif di Jawa

DigitShifter
sumber
0

Menggunakan File#createTempFiledan deletemembuat nama unik untuk direktori tampaknya ok. Anda harus menambahkan ShutdownHookuntuk menghapus direktori (secara rekursif) pada JVM shutdown.

ordnungswidrig
sumber
Hook shutdown rumit. Bukankah File # deleteOnExit juga berfungsi?
Daniel Hiller
2
#deleteOnExit tidak berfungsi untuk saya - saya yakin itu tidak akan menghapus direktori yang tidak kosong.
muriloq
Saya menerapkan tes cepat berjalan dengan Java 8, tetapi folder temp tidak dihapus, lihat pastebin.com/mjgG70KG
geri