Kotlin Files.copy

The Files class found in JDK provides a utility method that allows copying from an input stream into a file. The copy operation is found on lines 22 and 28. Here is an example program followed by a detailed explanation.

import java.io.Console
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption

private fun Path.isFile() = !Files.isDirectory(this)

private fun Path.exists() = Files.exists(this)

private fun replace(): Boolean {
        val console = console()
        val replace = console.readLine("File already exists! Enter Y to replace => ")
        return replace.toLowerCase() == "y"
}

private fun Path.doCopy(dest : Path) : Boolean {
    return if(isFile()){
        if(dest.exists()){
            if(replace()){
                //We need to pass StandardCopyOption.REPLACE_EXISTING when overwriting a file
                Files.copy(this, dest, StandardCopyOption.REPLACE_EXISTING)
                true
            } else {
                false
            }
        } else {
            Files.copy(this, dest)
            true
        }
    } else {
        false
    }
}

private fun console() : Console {
    val console = System.console()
    return if(console != null){
        console
    } else {
        println("Please run from the terminal")
        System.exit(-1)

        //Return needed for compiler but we never actually reach this statement
        console!!
    }
}

fun main(args : Array<String>){
    when (args.size){
        2 -> {
            val src = Paths.get(args[0])
            val dest = Paths.get(args[1])

            if(src.doCopy(dest)){
                println("Copied ${src.fileName} to ${dest.fileName}")
            }
        }
        else -> {
            println("Usage: src dest")
        }
    }
}

Extension Functions

The example program uses Kotlin’s extension functions to help simplify the code. For example, it seems more natural to call exists() on a Path object as opposed to Files.exists(path). Likewise, it seems more natual to call copy on a Path object rather than Files.copy(src, dest). For this reason, we define a number of extension functions in the program.

The first extension function is found on line 7.

private fun Path.isFile() = !Files.isDirectory(this)

There isn’t a lot of magic here. All we are doing is wrapping a call to Files.isDirectory inside of the extension function. It allows us to call path.isFile() later on.

The next function is equally as brief.

private fun Path.exists() = Files.exists(this)

Once again, we are writing this function so that we can call path.exists() later on in the program.

The next function is replace().

private fun replace(): Boolean {
        val console = console()
        val replace = console.readLine("File already exists! Enter Y to replace => ")
        return replace.toLowerCase() == "y"
}

This function is used by the program to prompt the user if the file already exists. As we will see, we need to pass StandardCopyOption.REPLACE_EXISTING to overwrite a file or it will throw an exception. The function makes a call to a console() function that returns a non-null Console object or exits the program.

private fun console() : Console {
    val console = System.console()
    return if(console != null){
        console
    } else {
        println("Please run from the terminal")
        System.exit(-1)

        //Return needed for compiler but we never actually reach this statement
        console!!
    }
}

The final extension function is doCopy(). This is the function that actually contains the call to Files.copy(), which is the topic of this post.

private fun Path.doCopy(dest : Path) : Boolean {
    return if(isFile()){
        if(dest.exists()){
            if(replace()){
                //We need to pass StandardCopyOption.REPLACE_EXISTING when overwriting a file
                Files.copy(this, dest, StandardCopyOption.REPLACE_EXISTING)
                true
            } else {
                false
            }
        } else {
            Files.copy(this, dest)
            true
        }
    } else {
        false
    }
}

The doCopy function uses our isFile() and exist() extension functions on both the src and dest Path objects. When isFile(), exists() and replace() return true, we make a call to Files.copy, passing in this as our source, dest as our path, and StandardCopyOption.REPLACE_EXISTING. The function will make a copy of the source Path (this) into the dest and overwrite the dest should it already exist.

The alternative case is used when the dest file doesn’t already exist. Since the destination file doesn’t exist, there is no need to prompt the user about replacing it and we do not need to pass any StandardCopyOption to the call to Files.copy(). The program will simply copy one file to the other and the function will return true.

The main() function is the final function in the program.

fun main(args : Array<String>){
    when (args.size){
        2 -> {
            val src = Paths.get(args[0])
            val dest = Paths.get(args[1])

            if(src.doCopy(dest)){
                println("Copied ${src.fileName} to ${dest.fileName}")
            }
        }
        else -> {
            println("Usage: src dest")
        }
    }
}

The main() function gets mention because it uses the previously discussed doCopy() extension function. When the function is true, we tell the user that we copied the files. Otherwise the program exits.

References

https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#copy-java.io.InputStream-java.nio.file.Path-java.nio.file.CopyOption…-
https://kotlinlang.org/docs/reference/extensions.html

Kotlin Files.readAttributes()

The Files.readAttributes() method comes from JDK and is used to return meta-data about a particular file.

import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.attribute.BasicFileAttributes

fun main(args : Array<String>){
    val path =
            if(args.isEmpty()){
                Paths.get(System.getProperty("user.dir"))
            } else {
                Paths.get(args[0])
            }

    if(Files.isDirectory(path)){

        Files.list(path).forEach({ it ->
            val attrs = Files.readAttributes(it, BasicFileAttributes::class.java)

            println("${it.fileName}")
            println("Size => ${attrs.size()}")
            println("Directory => ${attrs.isDirectory}")
            println("Regular File => ${attrs.isRegularFile}")
            println("Link => ${attrs.isSymbolicLink}")
            println("Last Accessed => ${attrs.lastAccessTime()}")
            println("Last Modified => ${attrs.lastModifiedTime()}")
            println()
        })
    } else {
        println("Enter a path to a directory")
    }
}

The readAttributes() call is made on line 16. The readAttributes() method takes a Path object and then a Java class object of BasicFileAttribures or one of its subinterfaces, DosFileAttributes or PosixFileAttributes. Depending on the Java class specified, the method will return either BasicFileAttributes, DosFileAttributes, or PosixFileAttributes. BasicFileAttributes is used in this example because it is portable across all systems, while the other two interfaces are specific to their respective platforms.

BasicFileAttributes provides many commonly used file attributes in a type safe fashion. We can access common attributes such as when the file was created, modified, or last accessed. The interface has boolean attributes to check if a Path is a directory or symbolic links. We can even check the size of the file with the BasicFileAttributes.

The DosFileAttributes interface has properties specific to DOS based platforms (i.e., Windows). We can check if a file is a system file, hidden file, read only, or archived. The PosixFileAttributes interface is used on unix based platforms such as Mac OS X or Linux. It contains properties file permissions, user groups, and the file owner.

References

https://docs.oracle.com/javase/7/docs/api/java/nio/file/attribute/BasicFileAttributes.html
https://docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html#readAttributes(java.nio.file.Path,%20java.lang.Class,%20java.nio.file.LinkOption…)

Koltin Files.getAttribute()

The Files class in JDK has a getAttribute() that accepts a Path object and returns a specified attribute.

import java.nio.file.Files
import java.nio.file.Paths

fun main(args: Array<String>) {
    val path =
            if (args.isEmpty()) {
                Paths.get(System.getProperty("user.dir"))
            } else {
                Paths.get(args[0])
            }
    if(Files.isDirectory(path)){

        Files.list(path).forEach({ it ->
            //getAttribute() asserts non-null, so we can add compiler null checks by declaring a nullable type if desired
            val creationTime = Files.getAttribute(it, "creationTime")
            val lastModified = Files.getAttribute(it, "lastModifiedTime")
            val size = Files.getAttribute(it, "size")
            val dir = Files.getAttribute(it, "isDirectory")

            println("${it.fileName}")
            println("Creation Time => $creationTime")
            println("Last Modified => $lastModified")
            println("Size => $size")
            println("Directory => $dir")
            println()
        })
    } else {
        println("Please enter a path to a directory")
    }
}

The demonstration of Files.getAttribute() is found on lines 15 – 18. In each case, the getAttribute() method accepts a Path object, a string name of the attribute, and optional LinkOptions. The value returned is of type any, and the return type is assert not null (!!) operator. Since the names of the attributes is a string, there is the possibility that an exception could get thrown also.

The getAttribute() method can be convient, but it has short comings. Since it is a Java class, Kotlin interprets it as returning assert non-null. This may or may not be a problem. The java-doc makes no mention of a null return type, so it’s most likely safe to assume non-null types. However, it compiler checks are wanted or desirable, then we can declare nullable types.

More troubling is the fact that Strings are used for the attribute types. We have no protections against a typo such as “sizes” when we meant “size”. We will get a runtime exception in the event that attribute doesn’t exist. Alway check the java document prior to using the getAttribute() method to make sure the attribute is available first, keeping in mind that some attributes are platform specific.

Note that there is also a readAttributes() method, found in an upcoming post, that offers much better type safety than getAttribute(). However, the getAttribute() is useful in the case that we wish to check only on specific attribute (such as size), without having to use additional objects.

References

https://docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html#getAttribute(java.nio.file.Path,%20java.lang.String,%20java.nio.file.LinkOption…)