Подтвердить что ты не робот

Нечетное поведение при удалении файлов с Files.delete()

Пожалуйста, рассмотрите следующий пример Java-класса (pom.xml ниже):

package test.filedelete;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;

import org.apache.commons.io.IOUtils;

public class Main
{
    public static void main(String[] args) throws IOException
    {
        byte[] bytes = "testtesttesttesttesttesttesttesttesttest".getBytes();
        InputStream is = new ByteArrayInputStream(bytes);

        Path tempFileToBeDeleted = Files.createTempFile("test", "");
        OutputStream os = Files.newOutputStream(tempFileToBeDeleted);
        IOUtils.copy(is, os);

        deleteAndCheck(tempFileToBeDeleted);

        // breakpoint 1
        System.out.println("\nClosing stream\n");

        os.close();

        deleteAndCheck(tempFileToBeDeleted);
    }

    private static void deleteAndCheck(Path file) throws IOException
    {
        System.out.println("Deleting file: " + file);
        try
        {
            Files.delete(file);
        }
        catch (NoSuchFileException e)
        {
            System.out.println("No such file");
        }
        System.out.println("File really deleted: " + !Files.exists(file));

        System.out.println("Recreating deleted file ...");
        try
        {
            Files.createFile(file);
            System.out.println("Recreation successful");
        }
        catch (IOException e)
        {
            System.out.println("Recreation not possible, exception: " + e.getClass().getName());
        }
    }
}

Я пишу в FileOutputStream и пытаюсь удалить файл после этого, не закрывая сначала Stream. Это была моя оригинальная проблема и, конечно, неправильная, но это приводит к некоторым странным наблюдениям.

При запуске основного метода в Windows 7 он выдает следующий вывод:

Deleting file: C:\Users\MSCHAE~1\AppData\Local\Temp\test6100073603559201768
File really deleted: true
Recreating deleted file ...
Recreation not possible, exception: java.nio.file.AccessDeniedException

Closing stream

Deleting file: C:\Users\MSCHAE~1\AppData\Local\Temp\test6100073603559201768
No such file
File really deleted: true
Recreating deleted file ...
Recreation successful
  • Почему первый вызов метода Files.delete() не генерирует исключение?
  • Почему следующий вызов функции Files.exist() возвращает false?
  • Почему не удается создать файл заново?

Что касается последнего вопроса, я заметил, что файл все еще отображается в проводнике при остановке в точке останова. Когда вы завершаете JVM, файл все равно будет удален. После закрытия потока deleteAndCheck() работает должным образом.

Мне кажется, что удаление не распространяется на ОС до закрытия потока, и API файлов не отражает это правильно.

Может кто-нибудь объяснить, что здесь происходит?

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>test</groupId>
    <artifactId>filedelete</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
        </dependency>
    </dependencies>
</project>

Обновление для уточнения

Файл исчезает в проводнике Windows, если поток закрыт. И вызывается имя файла Files.delete() - последние триггеры операции, или если был вызван файл .delete(), не закрывая поток, а JVM завершается.

4b9b3361

Ответ 1

Вы можете удалить открытый файл?

Это совершенно справедливо, чтобы удалить запись каталога файла, когда файл открыт. В Unix это семантика по умолчанию, и Windows ведет себя FILE_SHARE_DELETE если FILE_SHARE_DELETE установлен на всех файловых дескрипторах, открытых для этого файла.

[Редактировать: Спасибо @couling за обсуждения и исправления]

Однако есть небольшая разница: Unix удаляет имя файла немедленно, а Windows удаляет имя файла только тогда, когда последний дескриптор закрыт. Однако он не позволяет открывать файл с тем же именем до тех пор, пока последний дескриптор (удаленного) файла не будет закрыт.

Пойди разберись...

Однако в обеих системах удаление файла не обязательно приводит к его удалению, оно все еще занимает место на диске, пока есть открытый дескриптор. Пространство, занимаемое файлом, освобождается только тогда, когда последний открытый дескриптор закрыт.

Экскурсия: Windows

Необходимость указания флага в Windows заставляет большинство людей думать, что Windows не может удалить открытые файлы, но на самом деле это не так. Это просто поведение по умолчанию.

CreateFile():

Позволяет последующим операциям открытия файла или устройства запрашивать доступ для удаления.

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

Если этот флаг не указан, но файл или устройство были открыты для доступа для удаления, функция завершается ошибкой. Примечание. Доступ к удалению позволяет выполнять операции удаления и переименования.

DeleteFile():

Функция DeleteFile помечает файл для удаления при закрытии. Следовательно, удаление файла не происходит до тех пор, пока последний дескриптор файла не будет закрыт. Последующие вызовы CreateFile для открытия файла завершаются с ошибкой ERROR_ACCESS_DENIED.

Наличие открытого дескриптора файла без имени является одним из наиболее типичных способов создания неназванных временных файлов: создайте новый файл, откройте его, удалите файл. Теперь у вас есть дескриптор файла, который никто другой не может открыть. В Unix имя файла действительно исчезло, а в Windows вы не можете открыть файл с таким же именем.

Вопрос сейчас:

Есть ли в Files.newOutputStream() значение FILE_SHARE_DELETE?

Глядя на источник, вы можете увидеть, что shareDelete действительно имеет значение true. Единственный способ сбросить его - использовать нестандартный ExtendedOpenOption NOSHARE_DELETE.

Так что да, вы можете удалять открытые файлы в Java, если они явно не заблокированы.

Почему я не могу заново создать удаленный файл?

Ответ на этот вопрос скрыт в документации к DeleteFile() выше: файл помечен только для удаления, файл все еще там. В Windows вы не можете создать файл с именем файла, помеченного для удаления, до тех пор, пока файл не будет удален должным образом, т.е. все дескрипторы файла будут закрыты.

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

Почему Files.exists() возвращает false?

Files.exists() в глубоком конце Windows открывает этот файл в какой-то момент, и мы уже знаем, что мы не можем повторно открыть удаленный, но все еще открытый файл в Windows.

Подробно: Java-код вызывает FileSystemProvider.checkAccess()) без аргументов, который вызывает WindowsFileSystemProvider.checkReadAccess() который сразу же пытается открыть файл и, следовательно, не удается. Из того, что я могу сказать, это путь, используемый при вызове Files.exist().

Существует также другой путь кода, который вызывает GetFileAttributeEx() для получения атрибутов файла. Еще раз, не задокументировано, что происходит, когда вы пытаетесь получить атрибуты удаленного, но еще не удаленного файла, но на самом деле вы не можете получить атрибуты файла, помеченного для удаления.

Догадываясь, я бы сказал, что GetFileAttributeEx() вызывает GetFileInformationByHandle() в какой-то момент, к которому он никогда не доберется, потому что он не может получить дескриптор файла в первую очередь.

Таким образом, после DeleteFile() файл DeleteFile() для большинства практических целей. Однако у него все еще есть имя, которое отображается в списках каталогов, и вы не можете открыть файл с тем же именем, пока у исходного файла не будут закрыты все его дескрипторы.

Это поведение более или менее согласовано, поскольку использование GetFileAttributes() для проверки существования файла фактически является проверкой доступности файла, которая интерпретируется как существование файла. FindFirstFile() (используется проводником Windows для определения списка файлов) находит имена файлов, но ничего не говорит о доступности имен.

Добро пожаловать в еще несколько странных петель в вашей голове.

Ответ 2

Если Files.delete не выбрал исключение, значит, он удалил файл. Files.delete javadoc говорит, что "на некоторых операционных системах может быть невозможно удалить файл, когда он открыт и используется этой виртуальной машиной Java или другими программами".