Kotlin I/O new features: relativeTo, copyTo, copyRecursively, deleteRecursively, file tree walkers,

file component iterators, file roots, startsWith, endsWith, subPath, normalize, replaceBytes, replaceTest,
additional tests and comments
This commit is contained in:
Denis Mekhanikov
2014-10-23 17:17:01 +04:00
committed by Mikhail Glukhikh
parent dae42f7c76
commit f560677b15
11 changed files with 1949 additions and 155 deletions
@@ -0,0 +1,45 @@
package kotlin.io
import java.io.File
import java.io.IOException
private fun constructMessage(file: File, other: File?, reason: String?): String {
val sb = StringBuilder(file.toString())
if (other != null) {
sb.append(" -> $other")
}
if (reason != null) {
sb.append(": $reason")
}
return sb.toString()
}
/**
* A base exception class for file system exceptions
*/
open public class FileSystemException(public val file: File,
public val other: File? = null,
public val reason: String? = null
) : IOException(constructMessage(file, other, reason))
/**
* An exception class which is used when some file to create or copy to already exists
*/
public class FileAlreadyExistsException(file: File,
other: File? = null,
reason: String? = null) : FileSystemException(file, other, reason)
/**
* An exception class which is used when we have not enough access for some operation
*/
public class AccessDeniedException(file: File,
other: File? = null,
reason: String? = null) : FileSystemException(file, other, reason)
/**
* An exception class which is used when file to copy does not exist
*/
public class NoSuchFileException(file: File,
other: File? = null,
reason: String? = null) : FileSystemException(file, other, reason)
-95
View File
@@ -1,95 +0,0 @@
package kotlin.io
import java.io.*
import java.nio.charset.*
import java.util.*
/**
* Recursively process this file and all children with the given block.
* Note that if this file doesn't exist, then the block will be executed on it anyway.
*/
public fun File.recurse(block: (File) -> Unit): Unit {
block(this)
listFiles()?.forEach { it.recurse(block) }
}
/**
* Returns `this` if this file is a directory, or the parent if it is a file inside a directory
*/
public val File.directory: File
get() = if (isDirectory()) this else getParentFile()!!
/**
* Returns the canonical path of this file.
*/
public val File.canonicalPath: String
get() = getCanonicalPath()
/**
* Returns the file name or "" for an empty name
*/
public val File.name: String
get() = getName()
/**
* Returns the file path or "" for an empty name
*/
public val File.path: String
get() = getPath()
/**
* Returns the extension of this file (not including the dot), or an empty string if it doesn't have one.
*/
public val File.extension: String
get() {
return name.substringAfterLast('.', "")
}
/**
* Returns true if the given [file] is in the same directory as this file or a descendant directory.
*/
public fun File.isDescendant(file: File): Boolean {
return file.directory.canonicalPath.startsWith(directory.canonicalPath)
}
/**
* Returns the relative path of the given descendant of this file if it is a descendant, or an empty string otherwise.
*/
public fun File.relativePath(descendant: File): String {
val prefix = directory.canonicalPath
val answer = descendant.canonicalPath
return if (answer.startsWith(prefix)) {
val prefixSize = prefix.length()
if (answer.length() > prefixSize) {
answer.substring(prefixSize + 1)
} else ""
} else {
answer
}
}
/**
* Copies this file to the given output [file].
* @param bufferSize the buffer size to use when copying.
* @return the number of bytes copied
*/
public fun File.copyTo(file: File, bufferSize: Int = defaultBufferSize): Long {
file.directory.mkdirs()
val input = FileInputStream(this)
return input.use<FileInputStream, Long>{
val output = FileOutputStream(file)
output.use<FileOutputStream, Long>{
input.copyTo(output, bufferSize)
}
}
}
/**
* Returns an array of files and directories in this directory that satisfy the specified [filter],
* or null if this file does not denote a directory.
*/
public fun File.listFiles(filter: (file: File) -> Boolean): Array<File>? = listFiles(
object : FileFilter {
override fun accept(file: File) = filter(file)
}
)
+17 -1
View File
@@ -5,9 +5,11 @@ import java.nio.charset.Charset
import java.nio.charset.CharsetDecoder
import java.nio.charset.CharsetEncoder
// Note: hasNext() is incorrect because available() means "number of available bytes WITHOUT BLOCKING"
/** Returns an [Iterator] of bytes in this input stream. */
public fun InputStream.iterator(): ByteIterator =
object: ByteIterator() {
object : ByteIterator() {
override fun hasNext(): Boolean = available() > 0
public override fun nextByte(): Byte = read().toByte()
@@ -75,3 +77,17 @@ public fun InputStream.readBytes(estimatedSize: Int = defaultBufferSize): ByteAr
this.copyTo(buffer)
return buffer.toByteArray()
}
/**
* Constructs a new FileInputStream of this file and returns it as a result.
*/
public fun File.inputStream(): InputStream {
return FileInputStream(this)
}
/**
* Constructs a new FileOutputStream of this file and returns it as a result.
*/
public fun File.outputStream(): OutputStream {
return FileOutputStream(this)
}
+116 -18
View File
@@ -7,14 +7,16 @@ import java.util.ArrayList
import java.util.NoSuchElementException
/**
* Creates a new [FileReader] for reading the contents of this file.
* @return a new [FileReader] for reading the contents of this file.
*/
public fun File.reader(): FileReader = FileReader(this)
/**
* Reads the entire content of this file as a byte array.
*
* This method is not recommended on huge files.
* This method is not recommended on huge files. It has an internal limitation of 2 GB byte array size
*
* @return the entire content of this file as a byte array
*/
public fun File.readBytes(): ByteArray {
return FileInputStream(this).use { it.readBytes(length().toInt()) }
@@ -22,13 +24,27 @@ public fun File.readBytes(): ByteArray {
/**
* Replaces the contents of this file with [data].
*
* @data byte array to write into this file
*/
deprecated("Use replaceBytes instead")
public fun File.writeBytes(data: ByteArray): Unit {
return FileOutputStream(this).use { it.write(data) }
}
/**
* Replaces the contents of this file with [data].
*
* @data byte array to write into this file
*/
public fun File.replaceBytes(data: ByteArray): Unit {
return FileOutputStream(this).use { it.write(data) }
}
/**
* Appends [data] to the contents of this file.
*
* @data byte array to append to this file
*/
public fun File.appendBytes(data: ByteArray): Unit {
return FileOutputStream(this, true).use { it.write(data) }
@@ -37,33 +53,72 @@ public fun File.appendBytes(data: ByteArray): Unit {
/**
* Reads the entire content of this file as a String using specified [charset].
*
* This method is not recommended on huge files.
* This method is not recommended on huge files. It has an internal limitation of 2 GB file size
*
* @param charset character set to use
* @return the entire content of this file as a String
*/
deprecated("There is a same function with default parameter value")
public fun File.readText(charset: String): String = readBytes().toString(charset)
/**
* Reads the entire content of this file as a String using UTF-8 or specified [charset].
*
* This method is not recommended on huge files.
* This method is not recommended on huge files. It has an internal limitation of 2 GB file size
*
* @param charset character set to use
* @return the entire content of this file as a String
*/
public fun File.readText(charset: Charset = Charsets.UTF_8): String = readBytes().toString(charset)
/**
* Replaces the contents of this file with [text] encoded using the specified [charset].
*
* @param text text to write into file
* @param charset character set to use
*/
deprecated("Use replaceText instead")
public fun File.writeText(text: String, charset: String): Unit {
writeBytes(text.toByteArray(charset))
replaceBytes(text.toByteArray(charset))
}
/**
* Replaces the contents of this file with [text] encoded using the specified [charset].
*
* @param text text to write into file
* @param charset character set to use
*/
deprecated("There is a same function with default parameter value")
public fun File.replaceText(text: String, charset: String): Unit {
replaceBytes(text.toByteArray(charset))
}
/**
* Replaces the contents of this file with [text] encoded using UTF-8 or specified [charset].
*
* @param text text to write into file
* @param charset character set to use
*/
deprecated("Use replaceText instead")
public fun File.writeText(text: String, charset: Charset = Charsets.UTF_8): Unit {
writeBytes(text.toByteArray(charset))
replaceBytes(text.toByteArray(charset))
}
/**
* Replaces the contents of this file with [text] encoded using UTF-8 or specified [charset].
*
* @param text text to write into file
* @param charset character set to use
*/
public fun File.replaceText(text: String, charset: Charset = Charsets.UTF_8): Unit {
replaceBytes(text.toByteArray(charset))
}
/**
* Appends [text] to the contents of this file using UTF-8 or the specified [charset].
*
* @param text text to append to file
* @param charset character set to use
*/
public fun File.appendText(text: String, charset: Charset = Charsets.UTF_8): Unit {
appendBytes(text.toByteArray(charset))
@@ -71,7 +126,11 @@ public fun File.appendText(text: String, charset: Charset = Charsets.UTF_8): Uni
/**
* Appends [text] to the contents of the file using the specified [charset].
*
* @param text text to append to file
* @param charset character set to use
*/
deprecated("There is a same function with default parameter value")
public fun File.appendText(text: String, charset: String): Unit {
appendBytes(text.toByteArray(charset))
}
@@ -79,9 +138,11 @@ public fun File.appendText(text: String, charset: String): Unit {
/**
* Reads file by byte blocks and calls [closure] for each block read. Block size depends on implementation
* but is never less than 512 bytes.
* This functions passes the byte array and amount of bytes in this buffer to the [closure] function.
* This functions passes the byte array and amount of bytes in the array to the [closure] function.
*
* You can use this function for huge files.
*
* @param closure function to proceed file blocks
*/
public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit): Unit {
val arr = ByteArray(4096)
@@ -102,9 +163,13 @@ public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit): Unit {
}
/**
* Reads this file line by line using the specified [charset]. Default charset is UTF-8.
* Reads this file line by line using the specified [charset] and calls [closure] for each line.
* Default charset is UTF-8.
*
* You may use this function on huge files.
*
* @param charset character set to use
* @param closure function to proceed file lines
*/
public fun File.forEachLine(charset: Charset = Charsets.UTF_8, closure: (line: String) -> Unit): Unit {
val reader = BufferedReader(InputStreamReader(FileInputStream(this), charset))
@@ -116,23 +181,34 @@ public fun File.forEachLine(charset: Charset = Charsets.UTF_8, closure: (line: S
}
/**
* Reads this file line by line using the specified [charset].
* Reads this file line by line using the specified [charset] and calls [closure] for each line.
*
* You may use this function on huge files.
*
* @param charset character set to use
* @param closure function to proceed file lines
*/
deprecated("There is a same function with default parameter value")
public fun File.forEachLine(charset: String, closure: (line: String) -> Unit): Unit = forEachLine(Charset.forName(charset), closure)
/**
* Reads the file content as a list of lines, using the specified [charset].
*
* Do not use this function for huge files.
*
* @param charset character set to use
* @return list of file lines
*/
deprecated("There is a same function with default parameter value")
public fun File.readLines(charset: String): List<String> = readLines(Charset.forName(charset))
/**
* Reads the file content as a list of strings. By default uses UTF-8 charset.
* Reads the file content as a list of lines. By default uses UTF-8 charset.
*
* Do not use this function for huge files.
*
* @param charset character set to use
* @return list of file lines
*/
public fun File.readLines(charset: Charset = Charsets.UTF_8): List<String> {
val result = ArrayList<String>()
@@ -140,16 +216,19 @@ public fun File.readLines(charset: Charset = Charsets.UTF_8): List<String> {
return result
}
/** Creates a buffered reader wrapping this Reader, or returns this Reader if it is already buffered. */
/** @return a buffered reader wrapping this Reader, or this Reader itself if it is already buffered. */
public fun Reader.buffered(bufferSize: Int = defaultBufferSize): BufferedReader
= if (this is BufferedReader) this else BufferedReader(this, bufferSize)
/** Creates a buffered writer wrapping this Writer, or returns this Writer if it is already buffered. */
/** @return a buffered reader wrapping this Writer, or this Writer itself if it is already buffered. */
public fun Writer.buffered(bufferSize: Int = defaultBufferSize): BufferedWriter
= if (this is BufferedWriter) this else BufferedWriter(this, bufferSize)
/**
* Iterates through each line of this reader and closes the [Reader] when it's completed
* Iterates through each line of this reader, calls [block] for each line read
* and closes the [Reader] when it's completed
*
* @param block function to proceed file lines
*/
public fun Reader.forEachLine(block: (String) -> Unit): Unit = useLines { lines -> lines.forEach(block) }
@@ -162,7 +241,8 @@ public inline fun <T> Reader.useLines(block: (Sequence<String>) -> T): T =
this.buffered().use { block(it.lines()) }
/**
* Returns an iterator over each line.
* @return a stream of corresponding file lines
*
* *Note*: the caller must close the underlying `BufferedReader`
* when the iteration is finished; as the user may not complete the iteration loop (e.g. using a method like find() or any() on the iterator
* may terminate the iteration early.
@@ -204,6 +284,8 @@ private class LinesStream(private val reader: BufferedReader) : Sequence<String>
* Reads this reader completely as a String
*
* *Note*: It is the caller's responsibility to close this resource.
*
* @return the string with corresponding file contents
*/
public fun Reader.readText(): String {
val buffer = StringWriter()
@@ -212,9 +294,13 @@ public fun Reader.readText(): String {
}
/**
* Copies this reader to the given output writer, returning the number of bytes copied.
* Copies this reader to the given [out] writer, returning the number of bytes copied.
*
* **Note** it is the caller's responsibility to close both of these resources
*
* @param out writer to write to
* @param bufferSize size of character buffer to use in process
* @return number of bytes copies
*/
public fun Reader.copyTo(out: Writer, bufferSize: Int = defaultBufferSize): Long {
var charsCopied: Long = 0
@@ -229,16 +315,23 @@ public fun Reader.copyTo(out: Writer, bufferSize: Int = defaultBufferSize): Long
}
/**
* Reads the entire content of the URL as a String using the specified [charset].
* Reads the entire content of this URL as a String using the specified [charset].
*
* This method is not recommended on huge files.
*
* @param charset a character set to use
* @return a string with this URL entire content
*/
deprecated("There is a same function with default parameter value")
public fun URL.readText(charset: String): String = readBytes().toString(charset)
/**
* Reads the entire content of the URL as a String using UTF-8 or the specified [charset].
* Reads the entire content of this URL as a String using UTF-8 or the specified [charset].
*
* This method is not recommended on huge files.
*
* @param charset a character set to use
* @return a string with this URL entire content
*/
public fun URL.readText(charset: Charset = Charsets.UTF_8): String = readBytes().toString(charset)
@@ -246,12 +339,17 @@ public fun URL.readText(charset: Charset = Charsets.UTF_8): String = readBytes()
* Reads the entire content of the URL as bytes
*
* This method is not recommended on huge files.
*
* @return a byte array with this URL entire content
*/
public fun URL.readBytes(): ByteArray = this.openStream()!!.use<InputStream, ByteArray>{ it.readBytes() }
/**
* Executes the given [block] on this resource and then closes it down correctly whether an exception
* Executes the given [block] function on this resource and then closes it down correctly whether an exception
* is thrown or not
*
* @block a function to proceed this closable resource
* @return the result of [block] function on this closable resource
*/
public inline fun <T : Closeable, R> T.use(block: (T) -> R): R {
var closed = false
@@ -0,0 +1,138 @@
package kotlin.io
import java.io.File
import java.util.NoSuchElementException
fun String.getRootName(): String {
var first = this.indexOf(File.separatorChar, 0)
if (first==0) {
if (length() > 1 && this[1]==File.separatorChar) {
// Network names like //my.host/home/something => //my.host/home/ should be root
first = this.indexOf(File.separatorChar, 2)
if (first>=0) {
first = this.indexOf(File.separatorChar, first+1)
if (first >= 0)
return this.substring(0, first+1)
}
}
return this.substring(0, 1)
}
// C:\
if (first > 0 && this[first-1]==':') {
first++
return this.substring(0, first)
}
// C:
if (first==-1 && this.endsWith(':'))
return this
return ""
}
val File.rootName: String
get() = separatorsToSystem().getRootName()
/**
* Returns root component of this abstract name, like / from /home/user, or C:\ from C:\file.tmp,
* or //my.host/home for //my.host/home/user,
* or null if this name is relative, like bar/gav
*/
public val File.root: File?
get() {
val name = rootName
return if (name.length() > 0) File(name) else null
}
/**
* Iterates over elements of file, e.g. for /foo/bar/gav
* we will have foo, bar and gav
*/
class FileIterator(private val f: File): Iterator<File> {
private val path = f.separatorsToSystem()
val root = path.getRootName()
// Begin from the last root character
private var separatorIndex = root.length()-1
private var nextIndex = separatorIndex
/**
* Returns the next element for this file iterator
* @throws NoSuchElementException if there is no next element
*/
override fun next(): File {
if (!hasNext()) {
throw NoSuchElementException()
}
val prevIndex = separatorIndex
separatorIndex = nextIndex
return File(path.substring(prevIndex+1, nextIndex))
}
/**
* Returns true if the next element is available for this file iterator,
* or false otherwise
*/
override fun hasNext(): Boolean {
if (nextIndex == separatorIndex) {
// Consider special "" case
if (separatorIndex==-1 && path.length()==0) {
nextIndex = 0
return true
}
// /foo/bar/gav/, next points to last /
if (nextIndex >= path.length()-1)
return false
nextIndex = path.indexOf(File.separatorChar, separatorIndex+1)
// To remove all "" like in the middle of /home//something/
if (nextIndex==separatorIndex+1) {
separatorIndex = nextIndex
return hasNext()
}
}
if (nextIndex == -1)
nextIndex = path.length()
return true
}
}
/**
* Contains number of components in this abstract path name,
* e.g. 3 in /home/user/tmp (home, user, tmp) or 0 in /.
* Empty path name has one component which is the empty name itself.
*/
public val File.nameCount: Int
get() {
var i = 0
for (elem in this) i++
return i
}
/**
* Returns a relative pathname which is a subsequence of this pathname,
* beginning from component [beginIndex], inclusive,
* ending at component [endIndex], exclusive.
* Number 0 belongs to a component closest to the root,
* number count-1 belongs to a component farthest from the root
* @throws IllegalArgumentException if [beginIndex] is negative,
* or [endIndex] is greater than existing number of components,
* or [beginIndex] is greater than or equals to [endIndex]
*/
public fun File.subPath(beginIndex: Int, endIndex: Int): File {
if (beginIndex < 0 || beginIndex >= endIndex || endIndex > nameCount)
throw IllegalArgumentException()
var res = ""
var i = 0
for (elem in this) {
if (i >= beginIndex && i < endIndex) {
res += elem.toString()
if (i != endIndex-1)
res += File.separatorChar
}
i++
}
return File(res)
}
@@ -0,0 +1,251 @@
package kotlin.io.files
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.util.ArrayList
import java.util.NoSuchElementException
import java.util.Stack
/**
* Created by Mikhail.Glukhikh on 13/03/2015.
*
* An alternative version of file walker
*/
enum class FileWalkDirection {
/** Depth-first search, directory is visited BEFORE its files */
TOP_DOWN
/** Depth-first search, directory is visited AFTER its files */
BOTTOM_UP
// Do we want also breadth-first search?
}
class FileTreeWalk(private val start: File,
private val direction: FileWalkDirection = FileWalkDirection.TOP_DOWN,
private val enter: (File) -> Unit = {},
private val leave: (File) -> Unit = {},
private val fail: (f: File, e: IOException) -> Unit = { f, e -> Unit },
private val filter: (File) -> Boolean = { it -> true },
private val maxDepth: Int = Int.MAX_VALUE
): Stream<File> {
private abstract class DirectoryState(val rootDir: File) {
init {
if (!rootDir.isDirectory())
throw IllegalArgumentException("Directory is needed")
}
abstract public fun step(): File?
}
private inner class BottomUpDirectoryState(rootDir: File): DirectoryState(rootDir) {
private var rootVisited = false
private var fileList: Array<File>? = null
private var fileIndex = 0
private var failed = false
override public fun step(): File? {
if (!failed && fileList == null) {
enter(rootDir)
fileList = rootDir.listFiles()
if (fileList == null) {
fail(rootDir, AccessDeniedException(file = rootDir, reason = "Cannot list files in a directory"))
failed = true
}
}
if (fileList != null && fileIndex < fileList!!.size()) {
// First visit all files
return fileList!![fileIndex++]
} else if (!rootVisited) {
// Then visit root
rootVisited = true
return rootDir
} else {
// That's all
leave(rootDir)
return null
}
}
}
private inner class TopDownDirectoryState(rootDir: File): DirectoryState(rootDir) {
private var rootVisited = false
private var fileList: Array<File>? = null
private var fileIndex = 0
override public fun step(): File? {
if (!rootVisited) {
// First visit root
enter(rootDir)
rootVisited = true
return rootDir
} else if (fileList == null || fileIndex < fileList!!.size()) {
if (fileList == null) {
// Then read an array of files, if any
fileList = rootDir.listFiles()
if (fileList == null) {
fail(rootDir, AccessDeniedException(file = rootDir, reason = "Cannot list files in a directory"))
}
if (fileList == null || fileList!!.size() == 0) {
leave(rootDir)
return null
}
}
// Then visit all files
return fileList!![fileIndex++]
} else {
// That's all
leave(rootDir)
return null
}
}
}
// Stack of directory states, beginning from the start directory
private val state = Stack<DirectoryState>()
// We are already at the end or not?
private var end = false
// A future result of next() call
private var nextFile: File? = null
init {
if (!start.exists()) {
throw FileNotFoundException("The start file doesn't exist: $start")
}
if (start.isDirectory() && filter(start)) {
pushState(start)
}
}
private fun pushState(root: File) {
state.push(when (direction) {
FileWalkDirection.TOP_DOWN -> TopDownDirectoryState(root)
FileWalkDirection.BOTTOM_UP -> BottomUpDirectoryState(root)
})
}
private fun gotoNext(): File? {
if (end) {
// We are already at the end
return null
} else if (state.empty()) {
// There is nothing in the state
// We must visit "start" if it's a file and matches the filter
end = true
return if (!start.isDirectory() && filter(start)) start else null
}
// Take next file from the top of the stack
val topState = state.peek()
val file = topState.step()
if (file == null) {
// There is nothing more on the top of the stack, go back
state.pop()
return gotoNext()
} else {
// Check that file/directory matches the filter
if (!filter(file))
return gotoNext()
if (file == topState.rootDir || !file.isDirectory() || state.size() >= maxDepth) {
// Proceed a root directory or a simple file
return file
} else {
// Proceed a sub-directory
pushState(file)
return gotoNext()
}
}
}
/**
* Sets enter directory function.
* Enter function is called BEFORE the corresponding directory and its files are visited
*/
public fun enter(f: (File) -> Unit): FileTreeWalk {
return FileTreeWalk(start, direction, f, leave, fail, filter, maxDepth)
}
/**
* Sets leave directory function.
* Leave function is called AFTER the corresponding directory and its files are visited
*/
public fun leave(f: (File) -> Unit): FileTreeWalk {
return FileTreeWalk(start, direction, enter, f, fail, filter, maxDepth)
}
/**
* Set fail entering directory function.
* Fail function is called when walker is unable to get list of directory files.
* Enter and leave functions are called even in this case.
*/
public fun fail(f: (File, IOException) -> Unit): FileTreeWalk {
return FileTreeWalk(start, direction, enter, leave, f, filter, maxDepth)
}
/**
* Sets filter function.
* Filter function is called before visiting files and entering directories.
* If it returns false, file is not visited, directory is not entered and all its content is also not visited.
* If it returns true, everything goes in the regular way
*/
public fun filter(f: (File) -> Boolean): FileTreeWalk {
return FileTreeWalk(start, direction, enter, leave, fail, f, maxDepth)
}
/**
* Sets maximum depth of walk. Int.MAX_VALUE is used for unlimited.
* Negative and zero values are not allowed
*/
public fun maxDepth(depth: Int): FileTreeWalk {
if (depth <= 0)
throw IllegalArgumentException("Use positive depth value")
return FileTreeWalk(start, direction, enter, leave, fail, filter, depth)
}
private val it = object: Iterator<File> {
override fun hasNext(): Boolean {
if (nextFile == null)
nextFile = gotoNext()
return (nextFile != null)
}
override fun next(): File {
if (nextFile == null)
nextFile = gotoNext()
val res = nextFile
if (res == null)
throw NoSuchElementException()
// With nextFile = gotoNext() here some enter() / leave() can be called BEFORE visiting res
nextFile = null
// Visit this directory or file
return res
}
}
override fun iterator(): Iterator<File> {
return it
}
}
fun File.walk(direction: FileWalkDirection = FileWalkDirection.TOP_DOWN): FileTreeWalk =
FileTreeWalk(this, direction)
/**
* Gets a stream for visiting this directory and all its content in top-down order.
* Depth-first search is used and directories are visited before all their files
*/
public fun File.walkTopDown(): FileTreeWalk = walk(FileWalkDirection.TOP_DOWN)
/**
* Gets a stream for visiting this directory and all its content in bottom-up order.
* Depth-first search is used and directories are visited after all their files
*/
public fun File.walkBottomUp(): FileTreeWalk = walk(FileWalkDirection.BOTTOM_UP)
@@ -0,0 +1,514 @@
package kotlin.io
import java.io.*
import java.util.ArrayList
/**
* Creates an empty directory in the specified [directory], using the given [prefix] and [suffix] to generate its name.
*
* If [prefix] is not specified then some unspecified name will be used.
* If [suffix] is not specified then ".tmp" will be used.
* If [directory] is not specified then the default temporary-file directory will be used.
*
* @return a file object corresponding to a newly-created directory.
*
* @throws IOException in case of input/output error
* @throws IllegalArgumentException if [prefix] is shorter than three symbols
*/
public fun createTempDir(prefix: String = "tmp", suffix: String? = null, directory: File? = null): File {
val dir = File.createTempFile(prefix, suffix, directory)
dir.delete()
if (dir.mkdir()) {
return dir
} else {
throw IOException("Unable to create temporary directory")
}
}
/**
* Creates a new empty file in the specified [directory], using the given [prefix] and [suffix] to generate its name.
*
* If [prefix] is not specified then some unspecified name will be used.
* If [suffix] is not specified then ".tmp" will be used.
* If [directory] is not specified then the default temporary-file directory will be used.
*
* @return a file object corresponding to a newly-created file.
*
* @throws IOException in case of input/output error
* @throws IllegalArgumentException if [prefix] is shorter than three symbols
*/
public fun createTempFile(prefix: String = "tmp", suffix: String? = null, directory: File? = null): File {
return File.createTempFile(prefix, suffix, directory)
}
/**
* @return this if this file is a directory, or the parent if it is a file inside a directory
*
* Note: this property looks strange
*/
public val File.directory: File
get() = if (isDirectory()) this else parent!!
/**
* @return parent of this abstract path name, or null if it has no parent
*/
public val File.parent: File?
get() = getParentFile()
/**
* @return the canonical path of this file.
*/
public val File.canonicalPath: String
get() = getCanonicalPath()
/**
* @return the file name or "" for an empty name
*/
public val File.name: String
get() = getName()
/**
* @return the file path or "" for an empty name
*/
public val File.path: String
get() = getPath()
/**
* @return the extension of this file (not including the dot), or an empty string if it doesn't have one.
*/
public val File.extension: String
get() {
return name.substringAfterLast('.', "")
}
/**
* Replaces all separators in the string used to separate directories with system ones and returns the resulting string.
*
* @return the pathname with system separators
*/
public fun String.separatorsToSystem(): String {
val otherSep = if (File.separator == "/") "\\" else "/"
return replace(otherSep, File.separator)
}
/**
* Replaces all path separators in the string with system ones and returns the resulting string.
*
* @return the pathname with system separators
*/
public fun String.pathSeparatorsToSystem(): String {
val otherSep = if (File.pathSeparator == ":") ";" else ":"
return replace(otherSep, File.pathSeparator)
}
/**
* Replaces path and directories separators with corresponding system ones and returns the resulting string.
*
* @return the pathname with system separators
*/
public fun String.allSeparatorsToSystem(): String {
return separatorsToSystem().pathSeparatorsToSystem()
}
/**
* Returns a pathname of this file with all path separators replaced with File.pathSeparator
*
* @return the pathname with system separators
*/
public fun File.separatorsToSystem(): String {
return toString().separatorsToSystem()
}
/**
* @return file's name without an extension.
*/
public val File.nameWithoutExtension: String
get() = name.substringBeforeLast(".")
/**
* Calculates the relative path for this file from [base] file.
* Note that the [base] file is treated as a directory.
* If this file matches the [base] file, then an empty string will be returned.
*
* @return relative path from [base] to this
*
* @throws IllegalArgumentException if child and parent have different roots.
*/
public fun File.relativeTo(base: File): String {
// Check roots
if (root != base.root)
throw IllegalArgumentException("this and base files have different roots")
var it = iterator()
var baseIt = base.iterator()
var same = true
var sameComponents = 0
var baseComponents = 0
// Determine number of same components for this and base
// and total number of components for base
while (baseIt.hasNext()) {
baseComponents++
val baseName = baseIt.next()
if (same && it.hasNext()) {
if(it.next() == baseName) {
sameComponents++
} else {
same = false
}
} else {
same = false
}
}
// Add all ..
var res = ""
for (i in sameComponents+1..baseComponents-1)
res += (".." + File.separator)
val components = nameCount
// If .. is the last element, no separator should present
if (baseComponents > sameComponents) {
res += if (sameComponents < components) ".." + File.separator else ".."
}
// Add remaining this components
if (sameComponents < components-1)
res += (subPath(sameComponents, components-1).toString() + File.separator)
// The last one should be without separator
if (sameComponents < components)
res += subPath(components-1, components).toString()
return res
}
deprecated("Use relativeTo() function instead")
public fun File.relativePath(descendant: File): String {
val prefix = directory.canonicalPath
val answer = descendant.canonicalPath
return if (answer.startsWith(prefix)) {
val prefixSize = prefix.length()
if (answer.length() > prefixSize) {
answer.substring(prefixSize + 1)
} else ""
} else {
answer
}
}
/**
* Copies this file to the given output [dst], returning the number of bytes copied.
*
* If some directories on a way to the [dst] are missing, then they will be created.
* If the [dst] file already exists, then this function will fail unless [overwrite] argument is set to true and
* the [dst] file is not a non-empty directory.
*
* Note: this function fails if you call it on a directory.
* If you want to copy directories, use 'copyRecursively' function instead.
*
* @param overwrite true if destination overwrite is allowed
* @param bufferSize the buffer size to use when copying.
* @return the number of bytes copied
* @throws NoSuchFileException if the source file doesn't exist
* @throws FileAlreadyExistsException if the destination file already exists and 'rewrite' argument is set to false
* @throws IOException if any errors occur while copying
*/
public fun File.copyTo(dst: File, overwrite: Boolean = false, bufferSize: Int = defaultBufferSize): Long {
if (!exists()) {
throw NoSuchFileException(file = this, reason = "The source file doesn't exist")
} else if (isDirectory()) {
throw IllegalArgumentException("Use copyRecursively to copy a directory")
} else if (dst.exists()) {
if (!overwrite) {
throw FileAlreadyExistsException(file = this,
other = dst,
reason = "The destination file already exists")
} else if (dst.isDirectory() && dst.listFiles().any()) {
// In this case file should be copied *into* this directory,
// no matter whether it is empty or not
return copyTo(dst.resolve(name), overwrite, bufferSize)
}
}
dst.getParentFile().mkdirs()
dst.delete()
val input = FileInputStream(this)
return input.use<FileInputStream, Long> {
val output = FileOutputStream(dst)
output.use<FileOutputStream, Long> {
input.copyTo(output, bufferSize)
}
}
}
/**
* Enum that can be used to specify behaviour of the `copyRecursively()` function
* in exceptional conditions.
*/
public enum class OnErrorAction {
/** Skip this file and go to the next. */
SKIP
/** Terminate the evaluation of the function. */
TERMINATE
}
/**
* Copies this file with all its children to the specified destination [dst] path.
* If some directories on the way to the destination are missing, then they will be created.
*
* If any errors occur during the copying, then further actions will depend on the result of the call
* to `onError(File, IOException)` function, that will be called with arguments,
* specifying the file that caused the error and the exception itself.
* By default this function rethrows exceptions.
* Exceptions that can be passed to the `onError` function:
* NoSuchFileException - if there was an attempt to copy a non-existent file
* FileAlreadyExistsException - if there is a conflict
* AccessDeniedException - if there was an attempt to open a directory that didn't succeed.
* IOException - if some problems occur when copying.
*
* @return false if the copying was terminated, true otherwise.
*
* Note that if this function fails, then partial copying may have taken place.
*/
public fun File.copyRecursively(dst: File,
onError: (File, IOException) -> OnErrorAction =
{(file: File, e: IOException) -> throw e }
): Boolean {
fun copy(src: File): OnErrorAction? {
if (!src.exists()) {
return onError(this, NoSuchFileException(file = this, reason = "The source file doesn't exist"))
}
val relPath = src.relativeTo(this@copyRecursively)
val dstFile = File(dst, relPath)
if (dstFile.exists() && !(src.isDirectory() && dstFile.isDirectory())) {
return onError(dstFile, FileAlreadyExistsException(file = src,
other = dstFile,
reason = "The destination file already exists"))
}
try {
if (src.isDirectory()) {
dstFile.mkdirs()
val children = src.listFiles()
if (children == null) {
return onError(src, AccessDeniedException(file = src,
reason = "Cannot list files in a directory"))
}
for (child in children) {
val result = copy(child)
if (result == OnErrorAction.TERMINATE) {
return result
}
}
} else {
if (src.copyTo(dstFile, true) != src.length()) {
return onError(src, IOException("src.length() != dst.length()"))
}
}
} catch (e: IOException) {
return onError(src, e)
}
return null
}
val result = copy(this)
return result != OnErrorAction.TERMINATE
}
/**
* Delete this file with all its children.
*
* @return true if the file or directory is successfully deleted, false otherwise.
*
* Note that if this operation fails then partial deletion may have taken place.
*/
public fun File.deleteRecursively(): Boolean {
if (isDirectory()) {
listFiles()?.forEach { it.deleteRecursively() }
}
return delete()
}
/**
* @return an array of files and directories in the directory that match the specified [filter]
* or null if this file does not denote a directory.
*/
public fun File.listFiles(filter: (file: File) -> Boolean): Array<File>? = listFiles(
object : FileFilter {
override fun accept(file: File) = filter(file)
}
)
/**
* @return an iterator to go through pathname components, e.g.
* /foo/bar/gav has components foo, bar and gav
*/
public fun File.iterator(): Iterator<File> = FileIterator(this)
/**
* Determines whether this file belongs to the same root as [o]
* and starts with all components of [o] in the same order.
* So if [o] has N components, first N components of `this` must be the same as in [o].
* For relative [o], `this` can belong to any root.
*
* @return true if this path starts with [o] path, false otherwise
*/
public fun File.startsWith(o: File): Boolean {
val it = FileIterator(this)
val otherIt = FileIterator(o)
// Roots must be same OR other path can have no root
if (it.root != otherIt.root && otherIt.root != "")
return false
// Other path is empty
if (!otherIt.hasNext())
return true
val count = this.nameCount
val otherCount = o.nameCount
// This path is shorted than the other
if (count < otherCount)
return false
// Compare first elements until other ends
while (otherIt.hasNext()) {
if (it.next() != otherIt.next())
return false
}
return true
}
/**
* Determines whether this file belongs to the same root as [o]
* and starts with all components of [o] in the same order.
* So if [o] has N components, first N components of `this` must be the same as in [o].
* For relative [o], `this` can belong to any root.
*
* @return true if this path starts with [o] path, false otherwise
*/
public fun File.startsWith(o: String): Boolean = startsWith(File(o))
/**
* Determines whether this file belongs to the same root as [o]
* and ends with all components of [o] in the same order.
* So if [o] has N components, last N components of `this` must be the same as in [o].
* For relative [o], `this` can belong to any root.
*
* @return true if this path ends with [o] path, false otherwise
*/
public fun File.endsWith(o: File): Boolean {
val it = FileIterator(this)
val otherIt = FileIterator(o)
// Roots must be same OR other path can have no root
if (it.root != otherIt.root && otherIt.root != "")
return false
// Other path is empty
if (!otherIt.hasNext())
return true
var theirs = otherIt.next()
val count = this.nameCount
val otherCount = o.nameCount
// This path is shorted than the other
if (count < otherCount)
return false
var ours = it.next()
// Move forward to have same number of elements remaining
for (i in 0..count-otherCount-1) {
ours = it.next()
}
// Check all next elements are same, until the end
while (ours == theirs && it.hasNext() && otherIt.hasNext()) {
ours = it.next()
theirs = otherIt.next()
}
if (ours != theirs || it.hasNext() || otherIt.hasNext())
return false
return true
}
/**
* Determines whether this file belongs to the same root as [o]
* and ends with all components of [o] in the same order.
* So if [o] has N components, last N components of `this` must be the same as in [o].
* For relative [o], `this` can belong to any root.
*
* @return true if this path ends with [o] path, false otherwise
*/
public fun File.endsWith(o: String): Boolean = endsWith(File(o))
/**
* Removes all . and resolves all possible .. in this file name.
* For instance, File("/foo/./bar/gav/../baaz").normalize is File("/foo/bar/baaz")
*
* @return normalized pathname with . and possibly .. removed
*/
public fun File.normalize(): File {
val rootName = rootName
val list: MutableList<String> = ArrayList()
for (elem in this)
if (elem.toString() != ".")
list.add(elem.toString())
var first = 0
var dots = list.subList(first, list.size()).indexOf("..")
while (dots != -1) {
if (dots > 0) {
list.remove(dots+first)
list.remove(dots+first-1)
} else {
first++
}
dots = list.subList(first, list.size()).indexOf("..")
}
var res = rootName
var addSeparator = rootName != "" && !rootName.endsWith(File.separatorChar)
for (elem in list) {
if (addSeparator)
res += File.separatorChar
res += elem
addSeparator = true
}
return File(res)
}
/**
* Adds relative [o] to this, considering this as a directory.
* If [o] has a root, [o] is returned back.
* For instance, File("/foo/bar").resolve(File("gav")) is File("/foo/bar/gav").
* This function is complementary with (File.relativeTo),
* so f.resolve(g.relativeTo(f)) == g should be always true except for different roots case.
*
* @return concatenated this and [o] paths, or just [o] if it's absolute
*/
public fun File.resolve(o: File): File {
if (o.root != null)
return o
val ourName = toString()
if (ourName.endsWith(File.separatorChar))
return File(ourName + o)
else
return File(ourName + File.separatorChar + o)
}
/**
* Adds relative [o] to this, considering this as a directory.
* If [o] has a root, [o] is returned back.
* For instance, File("/foo/bar").resolve("gav") is File("/foo/bar/gav")
*
* @return concatenated this and [o] paths, or just [o] if it's absolute
*/
public fun File.resolve(o: String): File = resolve(File(o))
/**
* Adds relative [o] to this parent directory.
* If [o] has a root or this has no parent directory, [o] is returned back.
* For instance, File("/foo/bar").resolveSibling(File("gav")) is File("/foo/gav")
*
* @return concatenated this.parent and [o] paths, or just [o] if it's absolute or this has no parent
*/
public fun File.resolveSibling(o: File): File {
val parentFile = parent
if (parentFile != null)
return parentFile.resolve(o)
else
return o
}
/**
* Adds relative [o] to this parent directory.
* If [o] has a root or this has no parent directory, [o] is returned back
* For instance, File("/foo/bar").resolveSibling("gav") is File("/foo/gav")
*
* @return concatenated this.parent and [o] paths, or just [o] if it's absolute or this has no parent
*/
public fun File.resolveSibling(o: String): File = resolveSibling(File(o))