SwadhyAI Notes

All Java notes

Java Delete Files

›Short versionOpen if you only have a minute.
  • Files.delete removes one file or one empty directory. A directory that still has entries throws DirectoryNotEmptyException, and the directory stays.
  • File.delete returns false for a missing file, a permission failure, and a non-empty directory, with no type that says which.
  • deleteIfExists returns false only when the path was already absent. A non-empty directory still throws.
  • A tree comes out from Files.walk, sorted in reverse order, deleting each path. The walk deletes the directory you passed in, after its children.
  • The default walk does not follow a symlink. toRealPath() is the check that a user path has not left the root through .. or through a link.

removes one directory entry. The entry can be a file, an empty directory, or a symlink. A directory with anything still in it stays, and the call tells you so by throwing.

One file

fits a file you are finished with, including a part file left beside a note. A missing file is a quiet false.

DeleteNote.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class DeleteNote {
    public static void main(String[] args) throws IOException {
        Path file = Path.of("notes", "today.txt");
        if (Files.deleteIfExists(file)) {
            System.out.println("Deleted " + file.toAbsolutePath());
        } else {
            System.out.println("Already absent " + file.toAbsolutePath());
        }
    }
}

Files.delete on a missing path throws NoSuchFileException. Use that when absence is a bug. Use deleteIfExists when absence is a finished job. Either method throws AccessDeniedException when the process may not remove the entry. On Windows that includes a file another handle still has open.

File.delete() folds those outcomes into false. After a false return you know the entry is still there or was never there. You do not know which, and you do not know whether a retry would change anything.

A directory that still has files

rmdir on a directory that contains today.txt leaves the directory in place. Files.delete does the same and says why.

Terminal
$ rmdir notes
rmdir: notes: Directory not empty
$ ls notes
today.txt

The Java failure is . Catching it and continuing as if the tree were gone will leave today.txt on disk. To remove the tree, delete the entries inside, then the directory.

lists the tree. Reverse order deletes notes/today.txt before notes. The stream has to be closed, because it keeps directory handles open.

DeleteTree.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.stream.Stream;

static void deleteTree(Path dir) throws IOException {
    try (Stream<Path> walk = Files.walk(dir)) {
        for (Path path : walk.sorted(Comparator.reverseOrder()).toList()) {
            Files.delete(path);
        }
    }
}

The first path deleted is the deepest. The last is dir itself, which is empty by then. Point this method at a folder your program created. Pointed at a home directory, it removes that home directory after removing everything under it.

A under dir is one entry in the walk. Files.delete removes the link. The walk does not take FOLLOW_LINKS, so a link to another directory contributes its own name and nothing inside the target.

notes contains one symlink named link that points at /etc. You walk notes without FOLLOW_LINKS and delete every path. What remains of /etc?

All of it. The walk lists notes/link as a symlink, and Files.delete removes that link. /etc was the target, and the default walk never opened it. Passing FOLLOW_LINKS would visit /etc and delete entries there.

Stay inside the root

A delete aimed at a user string can leave the folder you meant to manage. .. leaves lexically. A symlink leaves after the file system resolves it. is the second check, and it applies because a delete talks about an entry that already exists.

DeleteUnderRoot.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

static void deleteUnderRoot(Path root, String userInput) throws IOException {
    Path realRoot = root.toAbsolutePath().normalize().toRealPath();
    Path candidate = realRoot.resolve(userInput).normalize();
    if (!candidate.startsWith(realRoot) || candidate.equals(realRoot)) {
        throw new IllegalArgumentException("path escapes the root");
    }
    Path real = candidate.toRealPath();
    if (!real.startsWith(realRoot)) {
        throw new IllegalArgumentException("path escapes the root");
    }
    Files.delete(real);
}

resolve of an absolute input drops realRoot. normalize turns ../../etc/passwd into a path outside the root. startsWith after that normalize rejects both. equals(realRoot) refuses a request whose cleaned path is the root itself, which would be a request to delete the whole tree.

The toRealPath() line is the symlink test. uploads/link can normalize to a path that still starts with uploads and resolve on disk to /etc/passwd. Deleting real without the second startsWith would delete that target. With the check, the call throws and /etc/passwd stays.

Files.delete(real) still refuses a directory that is not empty. Combining this guard with deleteTree means the tree you walk is the real path that survived both checks. Walk that real path, still without following links, so a link discovered deeper in the tree is removed as a link.

The path rules start on Java Files. Creating a file that does not exist yet uses the same root and stops at the parent, because a missing file has no real path.