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
@@ -374,6 +374,7 @@ public abstract class AbstractIncrementalJpsTest : JpsBuildTestCase() {
val file = File(workDir, path)
val oldLastModified = file.lastModified()
file.delete()
dataFile.copyTo(file)
val newLastModified = file.lastModified()
@@ -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))
+810 -35
View File
@@ -1,52 +1,582 @@
package test.io
import org.junit.Test as test
import java.io.File
import kotlin.test.assertEquals
import java.io.File
import java.io.IOException
import java.io.FileNotFoundException
import java.util.NoSuchElementException
import java.util.HashSet
import java.util.ArrayList
import kotlin.io.files.walkBottomUp
import kotlin.io.files.walkTopDown
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
class FilesTest {
test fun listFilesWithFilter() {
val dir = File.createTempFile("temp", System.nanoTime().toString())
dir.delete()
dir.mkdir()
File.createTempFile("temp1", ".kt", dir)
File.createTempFile("temp2", ".java", dir)
File.createTempFile("temp3", ".kt", dir)
test fun testPath() {
val fileSuf = System.currentTimeMillis().toString()
val file1 = createTempFile("temp", fileSuf)
assertTrue(file1.path.endsWith(fileSuf), file1.path)
}
test fun testCreateTempDir() {
val dirSuf = System.currentTimeMillis().toString()
val dir1 = createTempDir("temp", dirSuf)
assert(dir1.exists() && dir1.isDirectory() && dir1.name.startsWith("temp") && dir1.name.endsWith(dirSuf))
try {
createTempDir("a")
assert(false)
} catch(e: IllegalArgumentException) {}
val dir2 = createTempDir("temp")
assert(dir2.exists() && dir2.isDirectory() && dir2.name.endsWith(".tmp"))
val dir3 = createTempDir()
assert(dir3.exists() && dir3.isDirectory())
dir1.delete()
dir2.delete()
dir3.delete()
}
test fun testCreateTempFile() {
val fileSuf = System.currentTimeMillis().toString()
val file1 = createTempFile("temp", fileSuf)
assert(file1.exists() && file1.name.startsWith("temp") && file1.name.endsWith(fileSuf))
try {
createTempFile("a")
assert(false)
} catch(e: IllegalArgumentException) {}
val file2 = createTempFile("temp")
assert(file2.exists() && file2.name.endsWith(".tmp"))
val file3 = createTempFile()
assert(file3.exists())
file1.delete()
file2.delete()
file3.delete()
}
class Walks {
fun createTestFiles(): File {
val basedir = createTempDir()
File(basedir, "1").mkdir()
File(basedir, "1/2".separatorsToSystem()).mkdir()
File(basedir, "1/3".separatorsToSystem()).mkdir()
File(basedir, "1/3/4.txt".separatorsToSystem()).createNewFile()
File(basedir, "1/3/5.txt".separatorsToSystem()).createNewFile()
File(basedir, "6").mkdir()
File(basedir, "7.txt").createNewFile()
File(basedir, "8").mkdir()
File(basedir, "8/9.txt".separatorsToSystem()).createNewFile()
return basedir
}
test fun withSimple() {
val basedir = createTestFiles()
try {
val referenceNames =
listOf("", "1", "1/2", "1/3", "1/3/4.txt", "1/3/5.txt", "6", "7.txt", "8", "8/9.txt").map(
{it -> it.separatorsToSystem()}).toHashSet()
val namesTopDown = HashSet<String>()
for (file in basedir.walkTopDown()) {
val name = file.relativeTo(basedir)
assertFalse(namesTopDown.contains(name), "$name is visited twice")
namesTopDown.add(name)
}
assertEquals(referenceNames, namesTopDown)
val namesBottomUp = HashSet<String>()
for (file in basedir.walkBottomUp()) {
val name = file.relativeTo(basedir)
assertFalse(namesBottomUp.contains(name), "$name is visited twice")
namesBottomUp.add(name)
}
assertEquals(referenceNames, namesBottomUp)
} finally {
basedir.deleteRecursively()
}
}
test fun withEnterLeave() {
val basedir = createTestFiles()
try {
val referenceNames =
listOf("", "1", "1/2", "1/3", "6", "8").map(
{it -> it.separatorsToSystem()}).toHashSet()
val namesTopDownEnter = HashSet<String>()
val namesTopDownLeave = HashSet<String>()
val namesTopDown = HashSet<String>()
fun enter(file: File) {
val name = file.relativeTo(basedir)
assertFalse(namesTopDownEnter.contains(name), "$name is entered twice")
namesTopDownEnter.add(name)
assertFalse(namesTopDownLeave.contains(name), "$name is left before entrance")
}
fun leave(file: File) {
val name = file.relativeTo(basedir)
assertFalse(namesTopDownLeave.contains(name), "$name is left twice")
namesTopDownLeave.add(name)
assertTrue(namesTopDownEnter.contains(name), "$name is left before entrance")
}
fun visit(file: File) {
val name = file.relativeTo(basedir)
if (file.isDirectory()) {
assertTrue(namesTopDownEnter.contains(name), "$name is visited before entrance")
namesTopDown.add(name)
assertFalse(namesTopDownLeave.contains(name), "$name is visited after leaving")
}
if (file == basedir)
return
val parent = file.getParentFile()
if (parent != null) {
val parentName = parent.relativeTo(basedir)
assertTrue(namesTopDownEnter.contains(parentName),
"$name is visited before entering its parent $parentName")
assertFalse(namesTopDownLeave.contains(parentName),
"$name is visited after leaving its parent $parentName")
}
}
for (file in basedir.walkTopDown().enter(::enter).leave(::leave)) {
visit(file)
}
assertEquals(referenceNames, namesTopDownEnter)
assertEquals(referenceNames, namesTopDownLeave)
namesTopDownEnter.clear()
namesTopDownLeave.clear()
namesTopDown.clear()
for (file in basedir.walkBottomUp().enter(::enter).leave(::leave)) {
visit(file)
}
assertEquals(referenceNames, namesTopDownEnter)
assertEquals(referenceNames, namesTopDownLeave)
} finally {
basedir.deleteRecursively()
}
}
test fun withFilterAndMap() {
val basedir = createTestFiles()
try {
val referenceNames =
listOf("", "1", "1/2", "1/3", "6", "8").map(
{it -> it.separatorsToSystem()}).toHashSet()
assertEquals(referenceNames, basedir.walkTopDown().filter{ it.isDirectory() }.map{
it.relativeTo(basedir) }.toHashSet())
} finally {
basedir.deleteRecursively()
}
}
test fun withDeleteTxtTopDown() {
val basedir = createTestFiles()
try {
val referenceNames =
listOf("", "1", "1/2", "1/3", "6", "8").map(
{it -> it.separatorsToSystem()}).toHashSet()
val namesTopDown = HashSet<String>()
fun enter(file: File) {
assertTrue(file.isDirectory())
for (child in file.listFiles()) {
if (child.name.endsWith("txt"))
child.delete()
}
}
for (file in basedir.walkTopDown().enter(::enter)) {
val name = file.relativeTo(basedir)
assertFalse(namesTopDown.contains(name), "$name is visited twice")
namesTopDown.add(name)
}
assertEquals(referenceNames, namesTopDown)
} finally {
basedir.deleteRecursively()
}
}
test fun withDeleteTxtBottomUp() {
val basedir = createTestFiles()
try {
val referenceNames =
listOf("", "1", "1/2", "1/3", "6", "8").map(
{it -> it.separatorsToSystem()}).toHashSet()
val namesTopDown = HashSet<String>()
fun enter(file: File) {
assertTrue(file.isDirectory())
for (child in file.listFiles()) {
if (child.name.endsWith("txt"))
child.delete()
}
}
for (file in basedir.walkBottomUp().enter(::enter)) {
val name = file.relativeTo(basedir)
assertFalse(namesTopDown.contains(name), "$name is visited twice")
namesTopDown.add(name)
}
assertEquals(referenceNames, namesTopDown)
} finally {
basedir.deleteRecursively()
}
}
test fun withFilter() {
val basedir = createTestFiles()
try {
fun filter(file: File): Boolean {
// Everything ended with 3 is filtered
return (!file.name.endsWith("3"));
}
val referenceNames =
listOf("", "1", "1/2", "6", "7.txt", "8", "8/9.txt").map(
{it -> it.separatorsToSystem()}).toHashSet()
val namesTopDown = HashSet<String>()
for (file in basedir.walkTopDown().filter(::filter)) {
val name = file.relativeTo(basedir)
assertFalse(namesTopDown.contains(name), "$name is visited twice")
namesTopDown.add(name)
}
assertEquals(referenceNames, namesTopDown)
val namesBottomUp = HashSet<String>()
for (file in basedir.walkBottomUp().filter(::filter)) {
val name = file.relativeTo(basedir)
assertFalse(namesBottomUp.contains(name), "$name is visited twice")
namesBottomUp.add(name)
}
assertEquals(referenceNames, namesBottomUp)
} finally {
basedir.deleteRecursively()
}
}
test fun withTotalFilter() {
val basedir = createTestFiles()
try {
// Everything is filtered
fun filter(file: File) = false
val referenceNames: Set<String> = setOf()
val namesTopDown = HashSet<String>()
for (file in basedir.walkTopDown().filter(::filter)) {
val name = file.relativeTo(basedir)
assertFalse(namesTopDown.contains(name), "$name is visited twice")
namesTopDown.add(name)
}
assertEquals(referenceNames, namesTopDown)
val namesBottomUp = HashSet<String>()
for (file in basedir.walkBottomUp().filter(::filter)) {
val name = file.relativeTo(basedir)
assertFalse(namesBottomUp.contains(name), "$name is visited twice")
namesBottomUp.add(name)
}
assertEquals(referenceNames, namesBottomUp)
} finally {
basedir.deleteRecursively()
}
}
test fun withForEach() {
val basedir = createTestFiles()
try {
var i = 0
basedir.walkTopDown().forEach { it -> i++ }
assertEquals(10, i);
i = 0
basedir.walkBottomUp().forEach { it -> i++ }
assertEquals(10, i);
} finally {
basedir.deleteRecursively()
}
}
test fun withCount() {
val basedir = createTestFiles()
try {
assertEquals(10, basedir.walkTopDown().count());
assertEquals(10, basedir.walkBottomUp().count());
} finally {
basedir.deleteRecursively()
}
}
test fun withReduce() {
val basedir = createTestFiles()
try {
val res = basedir.walkTopDown().reduce { (a, b) -> if (a.canonicalPath > b.canonicalPath) a else b }
assertTrue(res.endsWith("9.txt"), "Expected end with 9.txt actual: ${res.name}")
} finally {
basedir.deleteRecursively()
}
}
test fun withVisitorAndDepth() {
val basedir = createTestFiles()
try {
val files = HashSet<String>()
val dirs = HashSet<String>()
val failed = HashSet<String>()
val stack = ArrayList<File>()
fun beforeVisitDirectory(dir: File) {
stack.add(dir)
dirs.add(dir.relativeTo(basedir))
}
fun afterVisitDirectory(dir: File) {
assertEquals(stack.last(), dir)
stack.remove(stack.lastIndex)
}
fun visitFile(file: File) {
assert(stack.last().listFiles().contains(file), file)
files.add(file.relativeTo(basedir))
}
fun visitDirectoryFailed(dir: File, e: IOException) {
assertEquals(stack.last(), dir)
stack.remove(stack.lastIndex)
failed.add(dir.name)
}
basedir.walkTopDown().enter(::beforeVisitDirectory).leave(::afterVisitDirectory).
fail(::visitDirectoryFailed).forEach{ it -> if (!it.isDirectory()) visitFile(it) }
assert(stack.isEmpty())
val sep = File.separator
for (fileName in array("", "1", "1${sep}2", "1${sep}3", "6", "8")) {
assert(dirs.contains(fileName), fileName)
}
for (fileName in array("1${sep}3${sep}4.txt", "1${sep}3${sep}4.txt", "7.txt", "8${sep}9.txt")) {
assert(files.contains(fileName), fileName)
}
//limit maxDepth
files.clear()
dirs.clear()
basedir.walkTopDown().enter(::beforeVisitDirectory).leave(::afterVisitDirectory).maxDepth(1).
forEach{ it -> if (it != basedir) visitFile(it) }
assert(stack.isEmpty())
assert(dirs.size() == 1 && dirs.contains(""), dirs.size())
for (file in array("1", "6", "7.txt", "8")) {
assert(files.contains(file), file)
}
//restrict access
if (File(basedir, "1").setReadable(false)) {
try {
files.clear()
dirs.clear()
basedir.walkTopDown().enter(::beforeVisitDirectory).leave(::afterVisitDirectory).
fail(::visitDirectoryFailed).forEach{ it -> if (!it.isDirectory()) visitFile(it) }
assert(stack.isEmpty())
assert(failed.size() == 1 && failed.contains("1"), failed.size())
assert(dirs.size() == 4, dirs.size())
for (dir in array("", "1", "6", "8")) {
assert(dirs.contains(dir), dir)
}
assert(files.size() == 2, files.size())
for (file in array("7.txt", "8${sep}9.txt")) {
assert(files.contains(file), file)
}
} finally {
File(basedir, "1").setReadable(true)
}
} else {
System.err.println("cannot restrict access")
}
} finally {
basedir.deleteRecursively()
}
}
test fun topDown() {
val basedir = createTestFiles()
try {
val visited = HashSet<File>()
val block: (File) -> Unit = {
assert(!visited.contains(it), it)
assert(it == basedir && visited.isEmpty() || visited.contains(it.getParentFile()), it)
visited.add(it)
}
basedir.walkTopDown().forEach( block )
assert(visited.size() == 10, visited.size())
} finally {
basedir.deleteRecursively()
}
}
test fun restrictedAccess() {
val basedir = createTestFiles()
val restricted = File(basedir, "1")
try {
if (restricted.setReadable(false)) {
val visited = HashSet<File>()
val block: (File) -> Unit = {
assert(!visited.contains(it), it)
assert(it == basedir && visited.isEmpty() || visited.contains(it.getParentFile()), it)
visited.add(it)
}
basedir.walkTopDown().forEach( block )
assert(visited.size() == 6, visited.size())
}
} finally {
restricted.setReadable(true)
basedir.deleteRecursively()
}
}
test fun backup() {
var count = 0
fun makeBackup(file: File) {
count++
val bakFile = File(file.toString() + ".bak")
file.copyTo(bakFile)
}
val basedir1 = createTestFiles()
try {
basedir1.walkTopDown().forEach {
if (it.isFile()) {
makeBackup(it)
}
}
assert(count == 4)
} finally {
basedir1.deleteRecursively()
}
count = 0
val basedir2 = createTestFiles()
try {
basedir2.walkTopDown().forEach {
if (it.isFile()) {
makeBackup(it)
}
}
assert(count == 4)
} finally {
basedir2.deleteRecursively()
}
}
test fun find() {
val basedir = createTestFiles()
try {
File(basedir, "8/4.txt".separatorsToSystem()).createNewFile()
var count = 0
basedir.walkTopDown().takeWhile{ it -> count == 0 }.forEach {
if (it.name == "4.txt") {
count++
}
}
assert(count == 1)
} finally {
basedir.deleteRecursively()
}
}
test fun findGits() {
val basedir = createTestFiles()
try {
File(basedir, "1/3/.git").mkdir()
File(basedir, "1/2/.git").mkdir()
File(basedir, "6/.git").mkdir()
val found = HashSet<File>()
for (file in basedir.walkTopDown()) {
if (file.name == ".git") {
found.add(file.getParentFile())
}
}
assert(found.size() == 3)
} finally {
basedir.deleteRecursively()
}
}
test fun streamFileTree() {
val dir = createTempDir()
try {
val subDir1 = createTempDir(prefix = "d1_", directory = dir)
val subDir2 = createTempDir(prefix = "d2_", directory = dir)
createTempDir(prefix = "d1_", directory = subDir1)
createTempFile(prefix = "f1_", directory = subDir1)
createTempDir(prefix = "d1_", directory = subDir2)
assertEquals(6, dir.walkTopDown().count())
} finally {
dir.deleteRecursively()
}
dir.mkdir()
try {
val it = dir.walkTopDown().iterator()
it.next()
it.next()
assert(false)
} catch(e: NoSuchElementException) {
} finally {
dir.delete()
}
try {
dir.walkTopDown()
assert(false)
} catch(e: FileNotFoundException) {}
}
}
test fun listFilesWithFilter() {
val dir = createTempDir("temp")
createTempFile("temp1", ".kt", dir)
createTempFile("temp2", ".java", dir)
createTempFile("temp3", ".kt", dir)
val result = dir.listFiles { it.getName().endsWith(".kt") }
assertEquals(2, result!!.size())
}
test fun recurse() {
val dir = File.createTempFile("temp", System.nanoTime().toString())
dir.delete()
test fun relativeToTest() {
val file1 = File("/foo/bar/baz")
val file2 = File("/foo/baa/ghoo")
assertEquals("../../bar/baz".separatorsToSystem(), file1.relativeTo(file2))
val file3 = File("/foo/bar")
assertEquals("baz", file1.relativeTo(file3))
assertEquals("..", file3.relativeTo(file1))
val file4 = File("/foo/bar/")
assertEquals("baz", file1.relativeTo(file4))
assertEquals("..", file4.relativeTo(file1))
assertEquals("", file3.relativeTo(file4))
assertEquals("", file4.relativeTo(file3))
val file5 = File("/foo/baran")
assertEquals("../bar".separatorsToSystem(), file3.relativeTo(file5))
assertEquals("../baran".separatorsToSystem(), file5.relativeTo(file3))
assertEquals("../bar".separatorsToSystem(), file4.relativeTo(file5))
assertEquals("../baran".separatorsToSystem(), file5.relativeTo(file4))
val file6 = File("C:\\Users\\Me")
val file7 = File("C:\\Users\\Me\\Documents")
assertEquals("..", file6.relativeTo(file7))
assertEquals("Documents", file7.relativeTo(file6))
val file8 = File("//my.host/home/user/documents/vip")
val file9 = File("//my.host/home/other/images/nice")
assertEquals("../../../user/documents/vip".separatorsToSystem(), file8.relativeTo(file9))
assertEquals("../../../other/images/nice".separatorsToSystem(), file9.relativeTo(file8))
val file10 = File("foo/bar")
val file11 = File("foo")
assertEquals("bar", file10.relativeTo(file11))
assertEquals("..", file11.relativeTo(file10))
}
var totalFiles = 0
dir.recurse { totalFiles++ }
assertEquals(1, totalFiles)
test fun relativeTo() {
assertEquals("kotlin", File("src/kotlin".separatorsToSystem()).relativeTo(File("src")))
assertEquals("", File("dir").relativeTo(File("dir")))
assertEquals("..", File("dir").relativeTo(File("dir/subdir".separatorsToSystem())))
assertEquals("../../test".separatorsToSystem(), File("test").relativeTo(File("dir/dir".separatorsToSystem())))
dir.mkdir()
File.createTempFile("temp", "1.kt", dir)
File.createTempFile("temp", "2.java", dir)
val subdir = File(dir, "subdir")
subdir.mkdir()
File(subdir, "3.txt").createNewFile()
totalFiles = 0
dir.recurse { totalFiles++ }
assertEquals(5, totalFiles)
if (subdir.setReadable(false)) {
// On Windows, we can't make directory not readable, and setReadable() will return false
totalFiles = 0
dir.recurse { totalFiles++ }
assertEquals(4, totalFiles)
// This test operates correctly only at Windows PCs with C & D drives
val file1 = File("C:/dir1".separatorsToSystem())
val file2 = File("D:/dir2".separatorsToSystem())
try {
val winRelPath = file1.relativeTo(file2)
assert(file1.canonicalPath.charAt(0) == '/')
assertEquals("../../C:/dir1", winRelPath)
} catch (e: IllegalArgumentException) {
assert(Character.isLetter(file1.canonicalPath.charAt(0)))
} catch (e: IOException) {
// The device is not ready (D) ==> DO NOTHING
}
}
@@ -59,4 +589,249 @@ class FilesTest {
assertEquals("", file1.relativePath(file1))
assertEquals(file3.canonicalPath, file1.relativePath(file3))
}
private fun checkFileElements(f: File, root: File?, elements: List<String>) {
var i = 0
assertEquals(root, f.root)
for (elem in f) {
assertTrue(i < elements.size())
assertEquals(elements[i++], elem.toString())
}
assertEquals(elements.size(), i)
}
test fun fileIterator() {
checkFileElements(File("/foo/bar"), File("/"), listOf("foo", "bar"))
checkFileElements(File("/foo/bar/gav"), File("/"), listOf("foo", "bar", "gav"))
checkFileElements(File("/foo/bar/gav/"), File("/"), listOf("foo", "bar", "gav"))
checkFileElements(File("bar/gav"), null, listOf("bar", "gav"))
checkFileElements(File("C:\\bar\\gav"), File("C:\\"), listOf("bar", "gav"))
checkFileElements(File("C:\\"), File("C:\\"), listOf())
checkFileElements(File("C:"), File("C:"), listOf())
checkFileElements(File("//host.ru/home/mike"), File("//host.ru/home"), listOf("mike"))
checkFileElements(File(""), null, listOf(""))
checkFileElements(File("."), null, listOf("."))
checkFileElements(File(".."), null, listOf(".."))
}
test fun startsWith() {
assertTrue(File("C:\\Users\\Me\\Temp\\Game").startsWith("C:\\Users\\Me"))
assertFalse(File("C:\\Users\\Me\\Temp\\Game").startsWith("C:\\Users\\He"))
assertTrue(File("C:\\Users\\Me").startsWith("C:\\"))
}
test fun endsWith() {
assertTrue(File("/foo/bar").endsWith("bar"))
assertTrue(File("/foo/bar").endsWith("/bar"))
assertTrue(File("/foo/bar/gav/bar").endsWith("/bar"))
assertTrue(File("/foo/bar/gav/bar").endsWith("/gav/bar"))
assertFalse(File("/foo/bar/gav").endsWith("/bar"))
assertFalse(File("foo/bar").endsWith("/bar"))
}
test fun subPath() {
assertEquals(File("mike"), File("//my.host.net/home/mike/temp").subPath(0, 1))
assertEquals(File("bar/gav"), File("/foo/bar/gav/hi").subPath(1, 3))
}
test fun normalize() {
assertEquals(File("/foo/bar/baaz"), File("/foo/./bar/gav/../baaz").normalize())
assertEquals(File("../../bar"), File("../foo/../../bar").normalize())
assertEquals(File("C:\\windows"), File("C:\\home\\..\\documents\\..\\windows").normalize())
assertEquals(File("foo"), File("gav/bar/../../foo").normalize())
}
test fun resolve() {
assertEquals(File("/foo/bar/gav"), File("/foo/bar").resolve("gav"))
assertEquals(File("/foo/bar/gav"), File("/foo/bar/").resolve("gav"))
assertEquals(File("/gav"), File("/foo/bar").resolve("/gav"))
assertEquals(File("C:\\Users\\Me\\Documents\\important.doc"),
File("C:\\Users\\Me").resolve("Documents\\important.doc"))
}
test fun resolveSibling() {
assertEquals(File("/foo/gav"), File("/foo/bar").resolveSibling("gav"))
assertEquals(File("/foo/gav"), File("/foo/bar/").resolveSibling("gav"))
assertEquals(File("/gav"), File("/foo/bar").resolveSibling("/gav"))
assertEquals(File("C:\\Users\\Me\\Documents\\important.doc"),
File("C:\\Users\\Me\\profile.ini").resolveSibling("Documents\\important.doc"))
}
test fun extension() {
assertEquals("bbb", File("aaa.bbb").extension)
assertEquals("", File("aaa").extension)
assertEquals("", File("aaa.").extension)
// maybe we should think that such files have name .bbb and no extension
assertEquals("bbb", File(".bbb").extension)
assertEquals("", File("/my.dir/log").extension)
}
test fun nameWithoutExtension() {
assertEquals("aaa", File("aaa.bbb").nameWithoutExtension)
assertEquals("aaa", File("aaa").nameWithoutExtension)
assertEquals("aaa", File("aaa.").nameWithoutExtension)
assertEquals("", File(".bbb").nameWithoutExtension)
assertEquals("log", File("/my.dir/log").nameWithoutExtension)
}
test fun separatorsToSystem() {
var path = "/aaa/bbb/ccc"
assertEquals(path.replace("/", File.separator), File(path).separatorsToSystem())
path = "C:\\Program Files\\My Awesome Program"
assertEquals(path.replace("\\", File.separator), File(path).separatorsToSystem())
path = "/Libraries\\Java:/Libraries/Python:/Libraries/Ruby"
assertEquals(path.replace(":", File.pathSeparator), path.pathSeparatorsToSystem())
path = "/Libraries\\Java;/Libraries/Python;/Libraries/Ruby"
assertEquals(path.replace(";", File.pathSeparator), path.pathSeparatorsToSystem())
path = "/Libraries\\Java;/Libraries/Python:\\Libraries/Ruby"
assertEquals(path.replace("/", File.separator).replace("\\", File.separator)
.replace(":", File.pathSeparator).replace(";", File.pathSeparator), path.allSeparatorsToSystem())
assertEquals("test", "test".allSeparatorsToSystem())
}
test fun testCopyTo() {
val srcFile = createTempFile()
val dstFile = createTempFile()
srcFile.replaceText("Hello, World!")
try {
srcFile.copyTo(dstFile)
assert(false)
} catch (e: FileAlreadyExistsException) {}
var len = srcFile.copyTo(dstFile, overwrite = true)
assertEquals(13L, len)
assertEquals(srcFile.readText(), dstFile.readText())
assert(dstFile.delete())
len = srcFile.copyTo(dstFile)
assertEquals(13L, len)
assertEquals(srcFile.readText(), dstFile.readText())
assert(dstFile.delete())
dstFile.mkdir()
val child = File(dstFile, "child")
child.createNewFile()
srcFile.copyTo(dstFile, overwrite = true)
assertEquals(13L, len)
val copy = dstFile.resolve(srcFile.name)
assertEquals(srcFile.readText(), copy.readText())
assert(srcFile.delete())
assert(child.delete() && copy.delete() && dstFile.delete())
try {
srcFile.copyTo(dstFile)
assert(false)
} catch (e: NoSuchFileException) {}
srcFile.mkdir()
try {
srcFile.copyTo(dstFile)
assert(false)
} catch (e: IllegalArgumentException) {}
srcFile.delete()
}
test fun deleteRecursively() {
val dir = createTempDir()
dir.delete()
dir.mkdir()
val subDir = File(dir, "subdir");
subDir.mkdir()
File(dir, "test1.txt").createNewFile()
File(subDir, "test2.txt").createNewFile()
assert(dir.deleteRecursively())
assert(!dir.exists())
assert(!dir.deleteRecursively())
}
test fun copyRecursively() {
val src = createTempDir()
val dst = createTempDir()
dst.delete()
fun check() {
for (file in src.walkTopDown()) {
val dstFile = File(dst, file.relativeTo(src))
assert(dstFile.exists())
if (dstFile.isFile()) {
assertEquals(file.readText(), dstFile.readText())
}
}
}
try {
val subDir1 = createTempDir(prefix = "d1_", directory = src)
val subDir2 = createTempDir(prefix = "d2_", directory = src)
createTempDir(prefix = "d1_", directory = subDir1)
val file1 = createTempFile(prefix = "f1_", directory = src)
val file2 = createTempFile(prefix = "f2_", directory = subDir1)
file1.replaceText("hello")
file2.replaceText("wazzup")
createTempDir(prefix = "d1_", directory = subDir2)
assert(src.copyRecursively(dst))
check()
try {
src.copyRecursively(dst)
assert(false)
} catch (e: FileAlreadyExistsException) {
}
var conflicts = 0
src.copyRecursively(dst) {
(file: File, e: IOException) ->
if (e is FileAlreadyExistsException) {
conflicts++
OnErrorAction.SKIP
} else {
throw e
}
}
assert(conflicts == 2)
if (subDir1.setReadable(false)) {
try {
dst.deleteRecursively()
var caught = false
assert(src.copyRecursively(dst) {
(file: File, e: IOException) ->
if (e is AccessDeniedException) {
caught = true
OnErrorAction.SKIP
} else {
throw e
}
})
assert(caught)
check()
} finally {
subDir1.setReadable(true)
}
}
src.deleteRecursively()
dst.deleteRecursively()
try {
src.copyRecursively(dst)
assert(false)
} catch (e: NoSuchFileException) {
}
assert(!src.copyRecursively(dst) {
(file: File, e: IOException) ->
OnErrorAction.TERMINATE
})
} finally {
src.deleteRecursively()
dst.deleteRecursively()
}
}
}
+29
View File
@@ -0,0 +1,29 @@
package test.io
import org.junit.Test as test
import java.io.Writer
import java.io.BufferedReader
import java.io.File
import kotlinhack.test.assertEquals
class IOStreamsTest {
test fun testGetStreamOfFile() {
val tmpFile = createTempFile()
var writer: Writer? = null
try {
writer = tmpFile.outputStream().writer()
writer!!.write("Hello, World!")
} finally {
writer?.close()
}
var act: String?
var reader: BufferedReader? = null
try {
reader = tmpFile.inputStream().reader().buffered()
act = reader!!.readLine()
} finally {
reader?.close()
}
assertEquals("Hello, World!", act)
}
}
+28 -6
View File
@@ -5,14 +5,17 @@ import java.io.File
import kotlin.test.assertEquals
import java.io.Reader
import java.io.StringReader
import java.net.URL
import java.util.ArrayList
import kotlin.test.assertFalse
import kotlin.test.assertTrue
fun sample(): Reader = StringReader("Hello\nWorld");
class ReadWriteTest {
test fun testAppendText() {
val file = File.createTempFile("temp", System.nanoTime().toString())
file.writeText("Hello\n")
file.replaceText("Hello\n")
file.appendText("World")
assertEquals("Hello\nWorld", file.readText())
@@ -60,29 +63,42 @@ class ReadWriteTest {
test fun file() {
val file = File.createTempFile("temp", System.nanoTime().toString())
val writer = file.outputStream().writer().buffered()
file.writeText("Hello\nWorld")
writer.write("Hello")
writer.newLine()
writer.write("World")
writer.close()
//file.replaceText("Hello\nWorld")
file.forEachBlock{ (arr: ByteArray, size: Int) ->
assertTrue(size >= 11 && size <= 12, size.toString())
assertTrue(arr.contains('W'.toByte()))
}
val list = ArrayList<String>()
file.forEachLine{
list.add(it)
}
assertEquals(arrayListOf("Hello", "World"), list)
val text = file.inputStream().reader().readText()
assertTrue(text.contains("Hello"))
assertTrue(text.contains("World"))
file.writeText("")
file.replaceText("")
var c = 0
file.forEachLine { c++ }
assertEquals(0, c)
file.writeText(" ")
file.replaceText(" ")
file.forEachLine { c++ }
assertEquals(1, c)
file.writeText(" \n")
file.replaceText(" \n")
c = 0
file.forEachLine { c++ }
assertEquals(1, c)
file.writeText(" \n ")
file.replaceText(" \n ")
c = 0
file.forEachLine { c++ }
assertEquals(2, c)
@@ -146,4 +162,10 @@ class ReadWriteTest {
assertEquals(arrayListOf("Hello", "World"), list)
}
test fun testURL() {
val url = URL("http://kotlinlang.org")
val text = url.readText()
assertFalse(text.isEmpty())
}
}