Paths - I/O

I/O Paths (the Path interface) are immutable representations of a real file or directory on a filesystem. These paths can be used to open a file for reading or writing, or perform various filesystem interactions.

All Path instances are also PurePath instances and can be passed around like so. Unlike PurePath, only the platform-specific implementation if a Path is available (and is an internal class), as well as any path implementations for various virtual filesystems.

Creation

A platform-specific Path object can be created using the methods on the Path companion object:

val path = Path.of("/etc/hosts")
val path = Path.of(b("/etc/passwd"))
val path = Path.of(PurePath.native("/etc/shadow"))

Warning

You can only create a Path from a platform PurePath.

File I/O

Changed in version 1.3.0: File code was commonised between platforms.

The contents of a file can be read using the Path.readAllBytes and Path.readAllString convenience methods:

val path = Path.of("/etc/hostname")
assert(path.readAllBytes() == b("MEMBER"))
assert(path.readAllString() == b("BLOCK"))

Files can also be written to using Path.writeBytes and Path.writeString. By default, these methods are atomic (either ALL content will be written to the file, or NO content will be written to the file), but this can be disabled by passing atomic = false to the functions.

val path = Path.of("/etc/hostname")
path.writeBytes(b("ITEM"))
path.writeString("SCHOOL")

If you don’t wish to read whole files into memory, you can open the path using Path.open:

val path = Path.of("/dev/urandom")
path.open(StandardOpenModes.READ) { it: FilesystemFile ->
    val bytes = it.readUpTo(16)
}

open provides a FilesystemFile to a lambda which is a union of BidirectionalStream | Seekable.

There also exists an overloaded Path.open that takes a ClosingScope. See Closeable and ClosingScope for more information.

Filesystem interaction

Path instances have various methods to interact with their filesystem.

There are several high level functions and extensions for the most common actions:

val path = Path.of("/some/file")
// high-level move, works across different filesystems
path.move(Path.of("/some/other/file"))
// high-level copy, copies files efficiently and recursively copies directories
path.copy(Path.of("/some/other/file2"))
// high-level delete, will unlink files/symlinks and recursively delete directories
path.delete()
// high-level symlink
path.symlinkTo(Path.of("/real/file"))

To see the underlying lower-level functions that power these extensions, check their source code.

The status of a file can be queried with various methods:

val path = Path.home().resolveChild(".config/alacritty/alacritty.yml")
// check if the file exists
assert(path.exists())
// get the size of the file
println("File size: ${path.size()}")
// probe its type
assert(path.isRegularFile(followSymlinks = true))
assert(!path.isDirectory(followSymlinks = false))
assert(!path.isLink())

For directories, there are two methods for listing the underlying files:

  • Path.scandir which is provided a lambda to be called for every entry (faster)

  • Path.listdir which returns a list of DirEntry instead.

The DirEntry data class contains a Path of the child directory and the FileType of the file listed (only supported on certain filesystems). It also contains functions similar to the query operations which operate on the FileType to avoid excessive stat() calls.

A Path can be fully resolved into an absolute path using resolveFully:

val path = Path.of("./abc/def")
val absolute = path.resolveFully()
assert(path.isAbsolute)
assert(path == Path.of("/home/cs/abc/def"))

Changed in version 1.2.0: This method was renamed from toAbsolutePath to reflect that it traverses symbolic links too.

Temporary files

Temporary folders and files are tricky security-wise (as people can intercept your creation and do evil things). The Path.createTempDirectory extension is provided that calls an underlying, more secure, platform call to create a temporary directory with the correct permissions for security.

Path.createTempDirectory("some-prefix") { tmp ->
    val file = tmp.resolveChild("some-file.txt")
    file.writeAllString("...")
}

The path will be automatically recursively deleted at the end of operations.

Warning

Not to be confused with the unsafe method that only takes a prefix and returns the created Path instead of passing it to a lambda.