Comment créer un répertoire / dossier temporaire en Java?

Réponses:

391

Si vous utilisez JDK 7, utilisez la nouvelle classe Files.createTempDirectory pour créer le répertoire temporaire.

Path tempDirWithPrefix = Files.createTempDirectory(prefix);

Avant JDK 7, cela devrait le faire:

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

Vous pouvez faire de meilleures exceptions (sous-classe IOException) si vous le souhaitez.

TofuBeer
la source
12
C'est dangereux. Java est connu pour ne pas supprimer les fichiers immédiatement, donc mkdir peut parfois échouer
Demiurg
4
@Demiurg Le seul cas où un fichier n'est pas supprimé immédiatement est sur Windows lorsque le fichier est déjà ouvert (il pourrait être ouvert par un antivirus par exemple). Avez-vous d'autres documents à montrer autrement (je suis curieux de savoir des choses comme ça :-)? Si cela se produit régulièrement, alors le code ci-dessus ne fonctionnera pas, s'il est rare, alors interrompre l'appel au code ci-dessus jusqu'à ce que la suppression se produise (ou que certains essais maximum soient atteints) fonctionnerait.
TofuBeer
6
@Demiurg Java est connu pour ne pas supprimer les fichiers immédiatement. C'est vrai, même si vous ne l'ouvrez pas. Donc, un moyen plus sûr est temp.delete(); temp = new File(temp.getPath + ".d"); temp.mkdir(); ..., temp.delete();.
Xiè Jìléi
102
Ce code a une condition de concurrence entre delete()et mkdir(): Un processus malveillant pourrait créer le répertoire cible dans l'intervalle (en prenant le nom du fichier récemment créé). Voir Files.createTempDir()pour une alternative.
Joachim Sauer
11
J'aime le ! pour se démarquer, trop facile pour le manquer. J'ai lu beaucoup de code écrit par des étudiants ... si (! I) est assez courant pour être ennuyeux :-)
TofuBeer
182

La bibliothèque Google Guava possède une tonne d'utilitaires utiles. L'une des notes ici est la classe Files . Il a un tas de méthodes utiles, notamment:

File myTempDir = Files.createTempDir();

Cela fait exactement ce que vous avez demandé en une seule ligne. Si vous lisez la documentation ici, vous verrez que l'adaptation proposée de File.createTempFile("install", "dir")introduit généralement des vulnérabilités de sécurité.

Spina
la source
Je me demande à quelle vulnérabilité vous faites référence. Cette approche ne semble pas créer une condition de concurrence critique car File.mkdir () est supposé échouer si un tel répertoire existe déjà (créé par un attaquant). Je ne pense pas non plus que cet appel suivra un lien symbolique malveillant. Pourriez-vous clarifier ce que vous vouliez dire?
abb
3
@abb: Je ne connais pas les détails de la condition de course qui sont mentionnés dans la documentation de Guava. Je soupçonne que la documentation est correcte étant donné qu'elle met spécifiquement en évidence le problème.
Spina
1
@abb Tu as raison. Tant que le retour de mkdir () est vérifié, il sera sécurisé. Le code que Spina pointe utilise cette méthode mkdir (). grepcode.com/file/repo1.maven.org/maven2/com.google.guava/guava/… . Il s'agit uniquement d'un problème potentiel sur les systèmes Unix lors de l'utilisation du répertoire / tmp car il a le bit collant activé.
Sarel Botha
@SarelBotha merci d'avoir rempli le blanc ici. Cela faisait longtemps que je me posais des questions à ce sujet.
Spina
168

Si vous avez besoin d' un répertoire temporaire pour les tests et que vous utilisez JUnit, @Ruleainsi TemporaryFolderrésout votre problème:

@Rule
public TemporaryFolder folder = new TemporaryFolder();

De la documentation :

La règle TemporaryFolder permet la création de fichiers et de dossiers dont la suppression est garantie à la fin de la méthode de test (qu'elle réussisse ou échoue)


Mise à jour:

Si vous utilisez JUnit Jupiter (version 5.1.1 ou supérieure), vous avez la possibilité d'utiliser JUnit Pioneer qui est le pack d'extension JUnit 5.

Copié de la documentation du projet :

Par exemple, le test suivant enregistre l'extension pour une seule méthode de test, crée et écrit un fichier dans le répertoire temporaire et vérifie son contenu.

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

Plus d'informations dans le JavaDoc et le JavaDoc de 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>

Mise à jour 2:

L' annotation @TempDir a été ajoutée à la version JUnit Jupiter 5.4.0 en tant que fonctionnalité expérimentale. Exemple copié à partir du Guide de l'utilisateur de 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
la source
8
Disponible depuis JUnit 4.7
Eduard Wirch le
Ne fonctionne pas dans JUnit 4.8.2 sur Windows 7! (Problème d'autorisation)
exception
2
@CraigRinger: Pourquoi est-il imprudent de se fier à cela?
Adam Parkin
2
@AdamParkin Honnêtement, je ne me souviens plus. L'explication échoue!
Craig Ringer
1
Le principal avantage de cette approche est que le répertoire est géré par JUnit (créé avant le test et supprimé récursivement après le test). Et ça marche. Si vous obtenez "dir temporaire pas encore créé", c'est peut-être parce que vous avez oublié @Rule ou le champ en mode non public.
Bogdan Calmac
42

Un code naïvement écrit pour résoudre ce problème souffre de conditions de concurrence, y compris plusieurs des réponses ici. Historiquement, vous pouvez bien réfléchir aux conditions de concurrence et l'écrire vous-même, ou vous pouvez utiliser une bibliothèque tierce comme Google's Guava (comme le suggère la réponse de Spina). Ou vous pouvez écrire du code buggy.

Mais depuis JDK 7, il y a de bonnes nouvelles! La bibliothèque standard Java elle-même fournit désormais une solution (non racée) fonctionnant correctement à ce problème. Vous voulez java.nio.file.Files # createTempDirectory () . De la documentation :

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

Crée un nouveau répertoire dans le répertoire spécifié, en utilisant le préfixe donné pour générer son nom. Le chemin résultant est associé au même système de fichiers que le répertoire donné.

Les détails sur la façon dont le nom du répertoire est construit dépend de l'implémentation et ne sont donc pas spécifiés. Dans la mesure du possible, le préfixe est utilisé pour construire les noms des candidats.

Cela résout efficacement l' ancien rapport de bogue embarrassant dans le traqueur de bogues Sun qui demandait juste une telle fonction.

Greg Price
la source
35

Il s'agit du code source de Files.createTempDir () de la bibliothèque Guava. Ce n'est nulle part aussi complexe que vous pourriez le penser:

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

Par défaut:

private static final int TEMP_DIR_ATTEMPTS = 10000;

Vois ici

Andres Kievsky
la source
28

Ne l'utilisez pas deleteOnExit()même si vous le supprimez explicitement ultérieurement.

Google 'deleteonexit is evil' pour plus d'informations, mais l'essentiel du problème est:

  1. deleteOnExit() supprime uniquement pour les arrêts JVM normaux, pas de plantages ou de tuer le processus JVM.

  2. deleteOnExit() supprime uniquement lors de l'arrêt de la machine virtuelle Java - n'est pas bon pour les processus serveur de longue durée car:

  3. Le plus mauvais de tous - deleteOnExit()consomme de la mémoire pour chaque entrée de fichier temporaire. Si votre processus s'exécute pendant des mois ou crée de nombreux fichiers temporaires en peu de temps, vous consommez de la mémoire et ne la libérez jamais tant que la JVM ne s'est pas arrêtée.

Développeur Dude
la source
1
Nous avons une JVM où les fichiers classe et jar obtiennent des fichiers cachés en dessous créés par la JVM, et ces informations supplémentaires prennent un certain temps à supprimer. Lorsque vous effectuez des redéploiements à chaud sur des conteneurs Web faisant exploser des fichiers WAR, la JVM peut littéralement prendre quelques minutes à nettoyer après avoir terminé, mais avant de se terminer après avoir fonctionné pendant quelques heures.
Thorbjørn Ravn Andersen
20

Depuis Java 1.7 createTempDirectory(prefix, attrs)et createTempDirectory(dir, prefix, attrs)sont inclus dansjava.nio.file.Files

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

chercheur
la source
14

C'est ce que j'ai décidé de faire pour mon propre code:

/**
 * 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
la source
2
Ce n'est pas sûr. Voir le commentaire de Joachim Sauer dans la première option (tout aussi précaire). Une manière appropriée de vérifier l'existence d'un fichier ou d'un répertoire et de saisir le nom de fichier, de manière atomique, consiste à créer le fichier ou le répertoire.
zbyszek
1
@zbyszek javadocs dit "L'UUID est généré à l'aide d'un générateur de nombres pseudo aléatoires cryptographiquement fort." Étant donné que comment un processus malveillant crée-t-il un répertoire du même nom entre exist () et mkdirs (). En fait, en regardant cela maintenant, je pense que mon test exist () pourrait être un peu idiot.
Keith
Keith: UUID étant sécurisé ou non n'est pas crucial dans ce cas. Il suffit que les informations sur le nom que vous avez interrogé "fuient". Par exemple, supposons que le fichier en cours de création se trouve sur un système de fichiers NFS et que l'attaquant peut écouter (passivement) les paquets. Ou l'état du générateur aléatoire a été divulgué. Dans mon commentaire, j'ai dit que votre solution est tout aussi précaire que la réponse acceptée, mais ce n'est pas juste: celle acceptée est triviale à vaincre avec inotify, et celle-ci est beaucoup plus difficile à vaincre. Néanmoins, dans certains scénarios, cela est certainement possible.
zbyszek
2
J'ai eu la même pensée et mis en œuvre une solution utilisant des bits UUID aléatoires comme celui-ci. Aucune vérification n'existe, une seule tentative de création - un RNG fort utilisé par la méthode randomUUID garantit à peu près aucune collision (peut être utilisé pour générer des clés primaires dans les tables DB, fait moi-même et jamais connu de collision), donc assez confiant. Si quelqu'un n'est pas sûr, consultez stackoverflow.com/questions/2513573/…
brabster
Si vous regardez l'implémentation de Java, ils génèrent simplement des noms aléatoires jusqu'à ce qu'il n'y ait pas de collision. Leurs tentatives maximales sont infinies. Donc, si quelqu'un de mal intentionné devait continuer à deviner le nom de votre fichier / répertoire, il ferait une boucle pour toujours. Voici un lien vers la source: hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/9fb81d7a2f16/src/share/… Je pensais que cela pourrait en quelque sorte verrouiller le système de fichiers afin qu'il puisse générer atomiquement un nom unique et créer le répertoire, mais je suppose que cela ne fait pas cela selon le code source.
dosentmatter
5

Eh bien, "createTempFile" crée réellement le fichier. Alors pourquoi ne pas simplement le supprimer d'abord, puis faire le mkdir dessus?

Paul Tomblin
la source
1
Vous devez toujours vérifier la valeur de retour pour mkdir (). Si c'est faux, cela signifie que le répertoire existait déjà. Cela peut entraîner des problèmes de sécurité, pensez donc à ce que cela génère ou non une erreur dans votre application.
Sarel Botha
1
Voir la note sur les conditions de concurrence dans l'autre réponse.
Volker Stolz
C'est ce que j'aime, sauf la course
Martin Wickman
4

Ce code devrait fonctionner raisonnablement bien:

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;
}
mat b
la source
3
Que faire si le répertoire existe déjà et que vous n'y avez pas accès en lecture / écriture ou si c'est un fichier normal? Vous avez également une condition de concurrence.
Jeremy Huiskamp
2
De plus, deleteOnExit ne supprimera pas les répertoires non vides.
Trenton
3

Comme discuté dans cette RFE et ses commentaires, vous pouvez appeler en tempDir.delete()premier. Ou vous pouvez utiliser System.getProperty("java.io.tmpdir")et créer un répertoire là-bas. Dans tous les cas, n'oubliez pas d'appeler tempDir.deleteOnExit(), sinon le fichier ne sera pas supprimé une fois que vous aurez terminé.

Michael Myers
la source
Cette propriété n'est-elle pas appelée "java.io.tmpdir", pas "... temp"? Voir java.sun.com/j2se/1.4.2/docs/api/java/io/File.html
Andrew Swan
Tout à fait. J'aurais dû vérifier avant de répéter ce que j'ai lu.
Michael Myers
Le java.io.tmpdir est partagé, vous devez donc faire tout le vaudou habituel pour éviter de marcher sur les orteils de quelqu'un d'autre.
Thorbjørn Ravn Andersen
3

Juste pour l'achèvement, c'est le code de la bibliothèque google guava. Ce n'est pas mon code, mais je pense qu'il est utile de le montrer ici dans ce fil.

  /** 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
la source
2

J'ai le même problème donc c'est juste une autre réponse pour ceux qui sont intéressés, et c'est similaire à l'une des réponses ci-dessus:

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

Et pour mon application, j'ai décidé d'ajouter une option pour effacer la température à la sortie, j'ai donc ajouté un crochet d'arrêt:

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

La méthode supprime tous les sous-répertoires et fichiers avant de supprimer le temp , sans utiliser la pile d'appels (qui est totalement facultative et vous pouvez le faire avec la récursivité à ce stade), mais je veux être du bon côté.

immotif
la source
2

Comme vous pouvez le voir dans les autres réponses, aucune approche standard n'est apparue. Par conséquent, vous avez déjà mentionné Apache Commons, je propose l'approche suivante en utilisant FileUtils d' 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;

}

Ceci est préférable car apache met en commun la bibliothèque qui se rapproche le plus de la "norme" demandée et fonctionne avec JDK 7 et les versions plus anciennes. Cela renvoie également une "ancienne" instance de fichier (qui est basée sur un flux) et non une "nouvelle" instance de chemin (qui est basée sur un tampon et qui serait le résultat de la méthode getTemporaryDirectory () de JDK7) -> Par conséquent, elle renvoie ce dont la plupart des gens ont besoin lorsque ils veulent créer un répertoire temporaire.

Carsten Engelke
la source
1

J'aime les multiples tentatives de création d'un nom unique, mais même cette solution n'exclut pas une condition de concurrence. Un autre processus peut se glisser après le test exists()et l' if(newTempDir.mkdirs())invocation de la méthode. Je n'ai aucune idée de comment sécuriser complètement cela sans recourir au code natif, qui, je suppose, est ce qui est enterré à l'intérieur File.createTempFile().

Chris Lott
la source
1

Avant Java 7, vous pouviez également:

File folder = File.createTempFile("testFileUtils", ""); // no suffix
folder.delete();
folder.mkdirs();
folder.deleteOnExit();
geri
la source
1
Beau code. Mais malheureusement, "deleteOnExit ()" ne fonctionnera pas, car Java ne peut pas supprimer tout le dossier à la fois. Vous devez supprimer tous les fichiers récursivement: /
Adam Taras
1

Essayez ce petit exemple:

Code:

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


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

Sortie de la console sur la machine Windows:
C: \ Users \ userName \ AppData \ Local \ Temp \ tmpDir2908538301081367877

Commentaire:
Files.createTempDirectory génère automatiquement un ID unique - 2908538301081367877.

Remarque:
lisez ce qui suit pour supprimer les répertoires récursivement:
Supprimer les répertoires récursivement en Java

DigitShifter
la source
0

Utiliser File#createTempFileet deletepour créer un nom unique pour le répertoire semble correct. Vous devez ajouter un ShutdownHookpour supprimer le répertoire (récursivement) lors de l'arrêt de la JVM.

ordnungswidrig
la source
Un crochet d'arrêt est encombrant. Le fichier # deleteOnExit ne fonctionnerait-il pas également?
Daniel Hiller
2
#deleteOnExit n'a pas fonctionné pour moi - je pense que cela ne supprimera pas les répertoires non vides.
muriloq
J'ai implémenté un test rapide fonctionnant avec Java 8, mais le dossier temporaire n'a pas été supprimé, voir pastebin.com/mjgG70KG
geri