Kotlin I/O review/M11 fixes: Stream --> Sequence, recurse() returned back,
additional helpers like File.bufferedReader() and String.byteInputStream(), copyRecursively / deleteRecursively were rewritten using FileTreeWalk, FilePathComponents introduced as a replacement of FileIterator, classes / methods / properties permissions fixed, Linux specific things, resolveSibling rewritten using FilePathComponents
This commit is contained in:
@@ -9,42 +9,61 @@ import java.io.BufferedReader
|
||||
*/
|
||||
public val defaultBufferSize: Int = 64 * 1024
|
||||
|
||||
/**
|
||||
* Returns the default block size for forEachBlock()
|
||||
*/
|
||||
public val defaultBlockSize: Int = 4096
|
||||
|
||||
/**
|
||||
* Returns the minimum block size for forEachBlock()
|
||||
*/
|
||||
public val minimumBlockSize: Int = 512
|
||||
|
||||
/** Prints the given message to the standard output stream. */
|
||||
public fun print(message: Any?) {
|
||||
System.out.print(message)
|
||||
}
|
||||
|
||||
/** Prints the given message to the standard output stream. */
|
||||
public fun print(message: Int) {
|
||||
System.out.print(message)
|
||||
}
|
||||
|
||||
/** Prints the given message to the standard output stream. */
|
||||
public fun print(message: Long) {
|
||||
System.out.print(message)
|
||||
}
|
||||
|
||||
/** Prints the given message to the standard output stream. */
|
||||
public fun print(message: Byte) {
|
||||
System.out.print(message)
|
||||
}
|
||||
|
||||
/** Prints the given message to the standard output stream. */
|
||||
public fun print(message: Short) {
|
||||
System.out.print(message)
|
||||
}
|
||||
|
||||
/** Prints the given message to the standard output stream. */
|
||||
public fun print(message: Char) {
|
||||
System.out.print(message)
|
||||
}
|
||||
|
||||
/** Prints the given message to the standard output stream. */
|
||||
public fun print(message: Boolean) {
|
||||
System.out.print(message)
|
||||
}
|
||||
|
||||
/** Prints the given message to the standard output stream. */
|
||||
public fun print(message: Float) {
|
||||
System.out.print(message)
|
||||
}
|
||||
|
||||
/** Prints the given message to the standard output stream. */
|
||||
public fun print(message: Double) {
|
||||
System.out.print(message)
|
||||
}
|
||||
|
||||
/** Prints the given message to the standard output stream. */
|
||||
public fun print(message: CharArray) {
|
||||
System.out.print(message)
|
||||
@@ -54,42 +73,52 @@ public fun print(message: CharArray) {
|
||||
public fun println(message: Any?) {
|
||||
System.out.println(message)
|
||||
}
|
||||
|
||||
/** Prints the given message and newline to the standard output stream. */
|
||||
public fun println(message: Int) {
|
||||
System.out.println(message)
|
||||
}
|
||||
|
||||
/** Prints the given message and newline to the standard output stream. */
|
||||
public fun println(message: Long) {
|
||||
System.out.println(message)
|
||||
}
|
||||
|
||||
/** Prints the given message and newline to the standard output stream. */
|
||||
public fun println(message: Byte) {
|
||||
System.out.println(message)
|
||||
}
|
||||
|
||||
/** Prints the given message and newline to the standard output stream. */
|
||||
public fun println(message: Short) {
|
||||
System.out.println(message)
|
||||
}
|
||||
|
||||
/** Prints the given message and newline to the standard output stream. */
|
||||
public fun println(message: Char) {
|
||||
System.out.println(message)
|
||||
}
|
||||
|
||||
/** Prints the given message and newline to the standard output stream. */
|
||||
public fun println(message: Boolean) {
|
||||
System.out.println(message)
|
||||
}
|
||||
|
||||
/** Prints the given message and newline to the standard output stream. */
|
||||
public fun println(message: Float) {
|
||||
System.out.println(message)
|
||||
}
|
||||
|
||||
/** Prints the given message and newline to the standard output stream. */
|
||||
public fun println(message: Double) {
|
||||
System.out.println(message)
|
||||
}
|
||||
|
||||
/** Prints the given message and newline to the standard output stream. */
|
||||
public fun println(message: CharArray) {
|
||||
System.out.println(message)
|
||||
}
|
||||
|
||||
/** Prints a newline to the standard output stream. */
|
||||
public fun println() {
|
||||
System.out.println()
|
||||
|
||||
@@ -20,7 +20,7 @@ private fun constructMessage(file: File, other: File?, reason: String?): String
|
||||
open public class FileSystemException(public val file: File,
|
||||
public val other: File? = null,
|
||||
public val reason: String? = null
|
||||
) : IOException(constructMessage(file, other, reason))
|
||||
) : IOException(constructMessage(file, other, reason))
|
||||
|
||||
/**
|
||||
* An exception class which is used when some file to create or copy to already exists
|
||||
|
||||
@@ -4,15 +4,40 @@ import java.io.*
|
||||
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"
|
||||
import java.util.NoSuchElementException
|
||||
|
||||
/** Returns an [Iterator] of bytes in this input stream. */
|
||||
deprecated("It's not recommended to iterate through input stream bytes")
|
||||
public fun InputStream.iterator(): ByteIterator =
|
||||
object : ByteIterator() {
|
||||
override fun hasNext(): Boolean = available() > 0
|
||||
|
||||
public override fun nextByte(): Byte = read().toByte()
|
||||
var nextByte = -1
|
||||
|
||||
var nextPrepared = false
|
||||
|
||||
var finished = false
|
||||
|
||||
private fun prepareNext() {
|
||||
if (!nextPrepared && !finished) {
|
||||
nextByte = read()
|
||||
nextPrepared = true
|
||||
finished = (nextByte == -1)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun hasNext(): Boolean {
|
||||
prepareNext()
|
||||
return !finished
|
||||
}
|
||||
|
||||
public override fun nextByte(): Byte {
|
||||
prepareNext()
|
||||
if (finished)
|
||||
throw NoSuchElementException("Input stream is over")
|
||||
val res = nextByte.toByte()
|
||||
nextPrepared = false
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,12 +53,21 @@ else
|
||||
/** Creates a reader on this input stream using UTF-8 or the specified [charset]. */
|
||||
public fun InputStream.reader(charset: Charset = Charsets.UTF_8): InputStreamReader = InputStreamReader(this, charset)
|
||||
|
||||
/** Creates a buffered reader on this input stream using UTF-8 or the specified [charset]. */
|
||||
public fun InputStream.bufferedReader(charset: Charset = Charsets.UTF_8): BufferedReader = reader(charset).buffered()
|
||||
|
||||
/** Creates a reader on this input stream using the specified [charset]. */
|
||||
public fun InputStream.reader(charset: String): InputStreamReader = InputStreamReader(this, charset)
|
||||
|
||||
/** Creates a buffered reader on this input stream using the specified [charset]. */
|
||||
public fun InputStream.bufferedReader(charset: String): BufferedReader = reader(charset).buffered()
|
||||
|
||||
/** Creates a reader on this input stream using the specified [decoder]. */
|
||||
public fun InputStream.reader(decoder: CharsetDecoder): InputStreamReader = InputStreamReader(this, decoder)
|
||||
|
||||
/** Creates a reader on this input stream using the specified [decoder]. */
|
||||
public fun InputStream.bufferedReader(decoder: CharsetDecoder): BufferedReader = reader(decoder).buffered()
|
||||
|
||||
/**
|
||||
* Creates a buffered output stream wrapping this stream.
|
||||
* @param bufferSize the buffer size to use.
|
||||
@@ -74,7 +108,7 @@ public fun InputStream.copyTo(out: OutputStream, bufferSize: Int = defaultBuffer
|
||||
*/
|
||||
public fun InputStream.readBytes(estimatedSize: Int = defaultBufferSize): ByteArray {
|
||||
val buffer = ByteArrayOutputStream(estimatedSize)
|
||||
this.copyTo(buffer)
|
||||
copyTo(buffer)
|
||||
return buffer.toByteArray()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,34 @@ import java.util.ArrayList
|
||||
import java.util.NoSuchElementException
|
||||
|
||||
/**
|
||||
* @return a new [FileReader] for reading the contents of this file.
|
||||
* Returns a new [FileReader] for reading the content of this file.
|
||||
*/
|
||||
public fun File.reader(): FileReader = FileReader(this)
|
||||
|
||||
/**
|
||||
* Returns a new [BufferedReader] for reading the content of this file.
|
||||
*
|
||||
* @param bufferSize necessary size of the buffer
|
||||
*/
|
||||
public fun File.bufferedReader(bufferSize: Int = defaultBufferSize): BufferedReader = reader().buffered(bufferSize)
|
||||
|
||||
/**
|
||||
* Returns a new [FileWriter] for writing the content of this file.
|
||||
*/
|
||||
public fun File.writer(): FileWriter = FileWriter(this)
|
||||
|
||||
/**
|
||||
* Returns a new [BufferedWriter] for writing the content of this file.
|
||||
*
|
||||
* @param bufferSize necessary size of the buffer
|
||||
*/
|
||||
public fun File.bufferedWriter(bufferSize: Int = defaultBufferSize): BufferedWriter = writer().buffered(bufferSize)
|
||||
|
||||
/**
|
||||
* Returns a new [PrintWriter] for writing the content of this file.
|
||||
*/
|
||||
public fun File.printWriter(): PrintWriter = PrintWriter(writer().buffered())
|
||||
|
||||
/**
|
||||
* Reads the entire content of this file as a byte array.
|
||||
*
|
||||
@@ -23,9 +47,9 @@ public fun File.readBytes(): ByteArray {
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the contents of this file with [data].
|
||||
* Replaces the content of this file with [data].
|
||||
*
|
||||
* @data byte array to write into this file
|
||||
* @param data byte array to write into this file
|
||||
*/
|
||||
deprecated("Use replaceBytes instead")
|
||||
public fun File.writeBytes(data: ByteArray): Unit {
|
||||
@@ -33,18 +57,18 @@ public fun File.writeBytes(data: ByteArray): Unit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the contents of this file with [data].
|
||||
* Replaces the content of this file with [data].
|
||||
*
|
||||
* @data byte array to write into this file
|
||||
* @param 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.
|
||||
* Appends [data] to the content of this file.
|
||||
*
|
||||
* @data byte array to append to this file
|
||||
* @param data byte array to append to this file
|
||||
*/
|
||||
public fun File.appendBytes(data: ByteArray): Unit {
|
||||
return FileOutputStream(this, true).use { it.write(data) }
|
||||
@@ -58,7 +82,6 @@ public fun File.appendBytes(data: ByteArray): Unit {
|
||||
* @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)
|
||||
|
||||
/**
|
||||
@@ -72,7 +95,7 @@ public fun File.readText(charset: String): String = readBytes().toString(charset
|
||||
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].
|
||||
* Replaces the content of this file with [text] encoded using the specified [charset].
|
||||
*
|
||||
* @param text text to write into file
|
||||
* @param charset character set to use
|
||||
@@ -83,7 +106,7 @@ public fun File.writeText(text: String, charset: String): Unit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the contents of this file with [text] encoded using the specified [charset].
|
||||
* Replaces the content of this file with [text] encoded using the specified [charset].
|
||||
*
|
||||
* @param text text to write into file
|
||||
* @param charset character set to use
|
||||
@@ -94,7 +117,7 @@ public fun File.replaceText(text: String, charset: String): Unit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the contents of this file with [text] encoded using UTF-8 or specified [charset].
|
||||
* Replaces the content 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
|
||||
@@ -105,7 +128,7 @@ public fun File.writeText(text: String, charset: Charset = Charsets.UTF_8): Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the contents of this file with [text] encoded using UTF-8 or specified [charset].
|
||||
* Replaces the content 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
|
||||
@@ -115,7 +138,7 @@ public fun File.replaceText(text: String, charset: Charset = Charsets.UTF_8): Un
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends [text] to the contents of this file using UTF-8 or the specified [charset].
|
||||
* Appends [text] to the content of this file using UTF-8 or the specified [charset].
|
||||
*
|
||||
* @param text text to append to file
|
||||
* @param charset character set to use
|
||||
@@ -125,35 +148,45 @@ public fun File.appendText(text: String, charset: Charset = Charsets.UTF_8): Uni
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends [text] to the contents of the file using the specified [charset].
|
||||
* Appends [text] to the content 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))
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads file by byte blocks and calls [closure] for each block read. Block size depends on implementation
|
||||
* but is never less than 512 bytes.
|
||||
* Reads file by byte blocks and calls [closure] for each block read.
|
||||
* Block has default size which is implementation-dependent.
|
||||
* 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)
|
||||
public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit): Unit = forEachBlock(closure, defaultBlockSize)
|
||||
|
||||
/**
|
||||
* Reads file by byte blocks and calls [closure] for each block read.
|
||||
* 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
|
||||
* @param blockSize size of a block, replaced by 512 if it's less, 4096 by default
|
||||
*/
|
||||
public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit, blockSize: Int): Unit {
|
||||
val arr = ByteArray(if (blockSize < minimumBlockSize) minimumBlockSize else blockSize)
|
||||
val fis = FileInputStream(this)
|
||||
|
||||
try {
|
||||
do {
|
||||
val size = fis.read(arr)
|
||||
if (size == -1) {
|
||||
if (size <= 0) {
|
||||
break
|
||||
} else if (size > 0) {
|
||||
} else {
|
||||
closure(arr, size)
|
||||
}
|
||||
} while (true)
|
||||
@@ -172,12 +205,8 @@ public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit): Unit {
|
||||
* @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))
|
||||
try {
|
||||
reader.forEachLine(closure)
|
||||
} finally {
|
||||
reader.close()
|
||||
}
|
||||
// Note: close is called at forEachLine
|
||||
BufferedReader(InputStreamReader(FileInputStream(this), charset)).forEachLine(closure)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,7 +217,6 @@ public fun File.forEachLine(charset: Charset = Charsets.UTF_8, closure: (line: S
|
||||
* @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)
|
||||
|
||||
/**
|
||||
@@ -199,7 +227,6 @@ public fun File.forEachLine(charset: String, closure: (line: String) -> Unit): U
|
||||
* @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))
|
||||
|
||||
/**
|
||||
@@ -216,11 +243,11 @@ public fun File.readLines(charset: Charset = Charsets.UTF_8): List<String> {
|
||||
return result
|
||||
}
|
||||
|
||||
/** @return a buffered reader wrapping this Reader, or this Reader itself if it is already buffered. */
|
||||
/** Returns 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)
|
||||
|
||||
/** @return a buffered reader wrapping this Writer, or this Writer itself if it is already buffered. */
|
||||
/** Returns 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)
|
||||
|
||||
@@ -230,7 +257,7 @@ public fun Writer.buffered(bufferSize: Int = defaultBufferSize): BufferedWriter
|
||||
*
|
||||
* @param block function to proceed file lines
|
||||
*/
|
||||
public fun Reader.forEachLine(block: (String) -> Unit): Unit = useLines { lines -> lines.forEach(block) }
|
||||
public fun Reader.forEachLine(block: (String) -> Unit): Unit = useLines { it.forEach(block) }
|
||||
|
||||
/**
|
||||
* Calls the [block] callback giving it a sequence of all the lines in this file and closes the reader once
|
||||
@@ -238,29 +265,31 @@ public fun Reader.forEachLine(block: (String) -> Unit): Unit = useLines { lines
|
||||
* @return the value returned by [block].
|
||||
*/
|
||||
public inline fun <T> Reader.useLines(block: (Sequence<String>) -> T): T =
|
||||
this.buffered().use { block(it.lines()) }
|
||||
buffered().use { block(it.lines()) }
|
||||
|
||||
/**
|
||||
* @return a stream of corresponding file lines
|
||||
* Returns a sequence 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.
|
||||
*
|
||||
* We suggest you try the method [useLines] instead which closes the stream when the processing is complete.
|
||||
*
|
||||
* @return a sequence of corresponding file lines
|
||||
*/
|
||||
public fun BufferedReader.lines(): Sequence<String> = LinesStream(this)
|
||||
|
||||
deprecated("Use lines() function which returns Stream<String>")
|
||||
deprecated("Use lines() function which returns Sequence<String>")
|
||||
public fun BufferedReader.lineIterator(): Iterator<String> = lines().iterator()
|
||||
|
||||
private class LinesStream(private val reader: BufferedReader) : Sequence<String> {
|
||||
override fun iterator(): Iterator<String> {
|
||||
override public fun iterator(): Iterator<String> {
|
||||
return object : Iterator<String> {
|
||||
private var nextValue: String? = null
|
||||
private var done = false
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
override public fun hasNext(): Boolean {
|
||||
if (nextValue == null && !done) {
|
||||
nextValue = reader.readLine()
|
||||
if (nextValue == null) done = true
|
||||
@@ -268,7 +297,7 @@ private class LinesStream(private val reader: BufferedReader) : Sequence<String>
|
||||
return nextValue != null
|
||||
}
|
||||
|
||||
public override fun next(): String {
|
||||
override public fun next(): String {
|
||||
if (!hasNext()) {
|
||||
throw NoSuchElementException()
|
||||
}
|
||||
@@ -285,7 +314,7 @@ private class LinesStream(private val reader: BufferedReader) : Sequence<String>
|
||||
*
|
||||
* *Note*: It is the caller's responsibility to close this resource.
|
||||
*
|
||||
* @return the string with corresponding file contents
|
||||
* @return the string with corresponding file content
|
||||
*/
|
||||
public fun Reader.readText(): String {
|
||||
val buffer = StringWriter()
|
||||
@@ -322,7 +351,6 @@ public fun Reader.copyTo(out: Writer, bufferSize: Int = defaultBufferSize): Long
|
||||
* @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)
|
||||
|
||||
/**
|
||||
@@ -342,13 +370,13 @@ public fun URL.readText(charset: Charset = Charsets.UTF_8): String = readBytes()
|
||||
*
|
||||
* @return a byte array with this URL entire content
|
||||
*/
|
||||
public fun URL.readBytes(): ByteArray = this.openStream()!!.use<InputStream, ByteArray>{ it.readBytes() }
|
||||
public fun URL.readBytes(): ByteArray = openStream().use<InputStream, ByteArray>{ it.readBytes() }
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param 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 {
|
||||
@@ -358,7 +386,7 @@ public inline fun <T : Closeable, R> T.use(block: (T) -> R): R {
|
||||
} catch (e: Exception) {
|
||||
closed = true
|
||||
try {
|
||||
this.close()
|
||||
close()
|
||||
} catch (closeException: Exception) {
|
||||
// eat the closeException as we are already throwing the original cause
|
||||
// and we don't want to mask the real exception
|
||||
@@ -371,7 +399,7 @@ public inline fun <T : Closeable, R> T.use(block: (T) -> R): R {
|
||||
throw e
|
||||
} finally {
|
||||
if (!closed) {
|
||||
this.close()
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
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,100 @@
|
||||
package kotlin.io
|
||||
|
||||
import java.io.File
|
||||
import java.util.NoSuchElementException
|
||||
|
||||
private fun String.getRootName(): String {
|
||||
// Note: separators should be already replaced to system ones
|
||||
var first = indexOf(File.separatorChar, 0)
|
||||
println("$this.getRootName: first=$first")
|
||||
if (first == 0) {
|
||||
if (length() > 1) {
|
||||
// Network names like //my.host/home/something ? => //my.host/home/ should be root
|
||||
// We have to consider here also /my.host/home/something because on Unix
|
||||
// File converts // just to /
|
||||
first = indexOf(File.separatorChar, 2)
|
||||
if (first >= 0) {
|
||||
val dot = indexOf('.', 2)
|
||||
if (dot >= 0 && dot < first) {
|
||||
first = indexOf(File.separatorChar, first + 1)
|
||||
if (first >= 0)
|
||||
return substring(0, first + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return substring(0, 1)
|
||||
}
|
||||
// C:\
|
||||
if (first > 0 && this[first - 1] == ':') {
|
||||
first++
|
||||
return substring(0, first)
|
||||
}
|
||||
// C:
|
||||
if (first == -1 && endsWith(':'))
|
||||
return this
|
||||
return ""
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of 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 empty string if this name is relative, like bar/gav
|
||||
*/
|
||||
public 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
|
||||
}
|
||||
|
||||
public data class FilePathComponents(public val rootName: String, public val fileList: List<File>) {
|
||||
public val size: Int = fileList.size()
|
||||
|
||||
public fun subPath(beginIndex: Int, endIndex: Int): File {
|
||||
if (beginIndex < 0 || beginIndex >= endIndex || endIndex > size)
|
||||
throw IllegalArgumentException()
|
||||
|
||||
var res = ""
|
||||
var first = true
|
||||
for (elem in fileList.subList(beginIndex, endIndex)) {
|
||||
if (!first) {
|
||||
res += File.separatorChar
|
||||
} else {
|
||||
first = false
|
||||
}
|
||||
res += elem.toString()
|
||||
}
|
||||
return File(res)
|
||||
}
|
||||
}
|
||||
|
||||
public fun File.filePathComponents(): FilePathComponents {
|
||||
val path = separatorsToSystem()
|
||||
val rootName = path.getRootName()
|
||||
val subPath = path.substring(rootName.length())
|
||||
// if: a special case when we have only root component
|
||||
// Split not only by / or \, but also by //, ///, \\, \\\, etc.
|
||||
val list = if (rootName.length() > 0 && subPath.isEmpty()) listOf() else
|
||||
subPath.split("""\Q${File.separatorChar}\E+""").toList().map { it -> File(it) }
|
||||
return FilePathComponents(rootName, list)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = filePathComponents().subPath(beginIndex, endIndex)
|
||||
@@ -1,4 +1,4 @@
|
||||
package kotlin.io.files
|
||||
package kotlin.io
|
||||
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
@@ -13,7 +13,12 @@ import java.util.Stack
|
||||
* An alternative version of file walker
|
||||
*/
|
||||
|
||||
enum class FileWalkDirection {
|
||||
/**
|
||||
* An enumeration to describe possible walk directions.
|
||||
* There are two of them: beginning from parents, ending with children,
|
||||
* and beginning from children, ending with parents. Both use depth-first search.
|
||||
*/
|
||||
public enum class FileWalkDirection {
|
||||
/** Depth-first search, directory is visited BEFORE its files */
|
||||
TOP_DOWN
|
||||
/** Depth-first search, directory is visited AFTER its files */
|
||||
@@ -21,24 +26,31 @@ enum class FileWalkDirection {
|
||||
// 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> {
|
||||
/**
|
||||
* This class is intended to implement different file walk methods.
|
||||
* It allows to iterate through all files inside [start] directory.
|
||||
* If [start] is just a file, walker iterates only it.
|
||||
* If [start] does not exist, walker does not do any iterations at all.
|
||||
*/
|
||||
public 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 = { true },
|
||||
private val maxDepth: Int = Int.MAX_VALUE
|
||||
) : Sequence<File> {
|
||||
|
||||
private abstract class DirectoryState(val rootDir: File) {
|
||||
private abstract class DirectoryState(public 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 inner class BottomUpDirectoryState(rootDir: File) : DirectoryState(rootDir) {
|
||||
|
||||
private var rootVisited = false
|
||||
|
||||
@@ -72,7 +84,7 @@ class FileTreeWalk(private val start: File,
|
||||
}
|
||||
}
|
||||
|
||||
private inner class TopDownDirectoryState(rootDir: File): DirectoryState(rootDir) {
|
||||
private inner class TopDownDirectoryState(rootDir: File) : DirectoryState(rootDir) {
|
||||
|
||||
private var rootVisited = false
|
||||
|
||||
@@ -119,9 +131,8 @@ class FileTreeWalk(private val start: File,
|
||||
|
||||
init {
|
||||
if (!start.exists()) {
|
||||
throw FileNotFoundException("The start file doesn't exist: $start")
|
||||
}
|
||||
if (start.isDirectory() && filter(start)) {
|
||||
end = true
|
||||
} else if (start.isDirectory() && filter(start)) {
|
||||
pushState(start)
|
||||
}
|
||||
}
|
||||
@@ -133,6 +144,7 @@ class FileTreeWalk(private val start: File,
|
||||
})
|
||||
}
|
||||
|
||||
tailRecursive
|
||||
private fun gotoNext(): File? {
|
||||
if (end) {
|
||||
// We are already at the end
|
||||
@@ -141,7 +153,7 @@ class FileTreeWalk(private val start: File,
|
||||
// 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
|
||||
return if (start.exists() && !start.isDirectory() && filter(start)) start else null
|
||||
}
|
||||
// Take next file from the top of the stack
|
||||
val topState = state.peek()
|
||||
@@ -210,13 +222,14 @@ class FileTreeWalk(private val start: File,
|
||||
return FileTreeWalk(start, direction, enter, leave, fail, filter, depth)
|
||||
}
|
||||
|
||||
private val it = object: Iterator<File> {
|
||||
override fun hasNext(): Boolean {
|
||||
private val it = object : Iterator<File> {
|
||||
override public fun hasNext(): Boolean {
|
||||
if (nextFile == null)
|
||||
nextFile = gotoNext()
|
||||
return (nextFile != null)
|
||||
}
|
||||
override fun next(): File {
|
||||
|
||||
override public fun next(): File {
|
||||
if (nextFile == null)
|
||||
nextFile = gotoNext()
|
||||
val res = nextFile
|
||||
@@ -229,23 +242,38 @@ class FileTreeWalk(private val start: File,
|
||||
}
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<File> {
|
||||
override public fun iterator(): Iterator<File> {
|
||||
return it
|
||||
}
|
||||
}
|
||||
|
||||
fun File.walk(direction: FileWalkDirection = FileWalkDirection.TOP_DOWN): FileTreeWalk =
|
||||
/**
|
||||
* Gets a sequence for visiting this directory and all its content.
|
||||
*
|
||||
* @param direction walk direction, top-down (by default) or bottom-up
|
||||
*/
|
||||
public 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.
|
||||
* Gets a sequence 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.
|
||||
* Gets a sequence 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)
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param block the function to call on each file
|
||||
*/
|
||||
deprecated("It's recommended to use walkTopDown() / walkBottomUp()")
|
||||
public fun File.recurse(block: (File) -> Unit): Unit {
|
||||
walkTopDown().forEach { block(it) }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package kotlin.io
|
||||
|
||||
import java.io.*
|
||||
import java.nio.charset.Charset
|
||||
import java.util.ArrayList
|
||||
|
||||
/**
|
||||
@@ -11,7 +12,7 @@ import java.util.ArrayList
|
||||
* 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
|
||||
*/
|
||||
@@ -33,7 +34,7 @@ public fun createTempDir(prefix: String = "tmp", suffix: String? = null, directo
|
||||
* 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
|
||||
*/
|
||||
@@ -42,39 +43,37 @@ public fun createTempFile(prefix: String = "tmp", suffix: String? = null, direct
|
||||
}
|
||||
|
||||
/**
|
||||
* @return this if this file is a directory, or the parent if it is a file inside a directory
|
||||
*
|
||||
* Note: this property looks strange
|
||||
* 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 parent!!
|
||||
|
||||
/**
|
||||
* @return parent of this abstract path name, or null if it has no parent
|
||||
* Returns 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.
|
||||
* Returns the canonical path of this file.
|
||||
*/
|
||||
public val File.canonicalPath: String
|
||||
get() = getCanonicalPath()
|
||||
|
||||
/**
|
||||
* @return the file name or "" for an empty name
|
||||
* Returns the file name
|
||||
*/
|
||||
public val File.name: String
|
||||
get() = getName()
|
||||
|
||||
/**
|
||||
* @return the file path or "" for an empty name
|
||||
* Returns the file path
|
||||
*/
|
||||
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.
|
||||
* 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() {
|
||||
@@ -110,6 +109,12 @@ public fun String.allSeparatorsToSystem(): String {
|
||||
return separatorsToSystem().pathSeparatorsToSystem()
|
||||
}
|
||||
|
||||
/** Creates a new reader for the string */
|
||||
public fun String.reader(): StringReader = StringReader(this)
|
||||
|
||||
/** Creates a new byte input stream for the string */
|
||||
public fun String.byteInputStream(charset: Charset = Charsets.UTF_8): InputStream = ByteArrayInputStream(toByteArray(charset))
|
||||
|
||||
/**
|
||||
* Returns a pathname of this file with all path separators replaced with File.pathSeparator
|
||||
*
|
||||
@@ -120,7 +125,7 @@ public fun File.separatorsToSystem(): String {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return file's name without an extension.
|
||||
* Returns file's name without an extension.
|
||||
*/
|
||||
public val File.nameWithoutExtension: String
|
||||
get() = name.substringBeforeLast(".")
|
||||
@@ -131,51 +136,43 @@ public val File.nameWithoutExtension: String
|
||||
* 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)
|
||||
val components = filePathComponents()
|
||||
val baseComponents = base.filePathComponents()
|
||||
if (components.rootName != base.rootName)
|
||||
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
|
||||
}
|
||||
}
|
||||
var i = 0
|
||||
while (i < components.size && i < baseComponents.size && components.fileList[i] == baseComponents.fileList[i])
|
||||
i++
|
||||
val sameCount = i
|
||||
val baseCount = baseComponents.size
|
||||
// Add all ..
|
||||
var res = ""
|
||||
for (i in sameComponents+1..baseComponents-1)
|
||||
for (j in sameCount + 1..baseCount - 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 ".."
|
||||
if (baseCount > sameCount) {
|
||||
res += if (sameCount < components.size) ".." + File.separator else ".."
|
||||
}
|
||||
// Add remaining this components
|
||||
if (sameComponents < components-1)
|
||||
res += (subPath(sameComponents, components-1).toString() + File.separator)
|
||||
if (sameCount < components.size - 1)
|
||||
res += (components.subPath(sameCount, components.size - 1).toString() + File.separator)
|
||||
// The last one should be without separator
|
||||
if (sameComponents < components)
|
||||
res += subPath(components-1, components).toString()
|
||||
if (sameCount < components.size)
|
||||
res += components.subPath(components.size - 1, components.size).toString()
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the relative path for this file from [descendant] file.
|
||||
* Note that the [descendant] file is treated as a directory.
|
||||
* If this file matches the [descendant] directory or does not belong to it,
|
||||
* then an empty string will be returned.
|
||||
*/
|
||||
deprecated("Use relativeTo() function instead")
|
||||
public fun File.relativePath(descendant: File): String {
|
||||
val prefix = directory.canonicalPath
|
||||
@@ -192,7 +189,7 @@ public fun File.relativePath(descendant: File): String {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -215,8 +212,8 @@ public fun File.copyTo(dst: File, overwrite: Boolean = false, bufferSize: Int =
|
||||
} else if (dst.exists()) {
|
||||
if (!overwrite) {
|
||||
throw FileAlreadyExistsException(file = this,
|
||||
other = dst,
|
||||
reason = "The destination file already exists")
|
||||
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
|
||||
@@ -246,6 +243,8 @@ public enum class OnErrorAction {
|
||||
TERMINATE
|
||||
}
|
||||
|
||||
private class TerminateException(file: File) : FileSystemException(file) {}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -261,83 +260,68 @@ public enum class OnErrorAction {
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
{ file, e -> throw e }
|
||||
): Boolean {
|
||||
if (!exists()) {
|
||||
return onError(this, NoSuchFileException(file = this, reason = "The source file doesn't exist")) !=
|
||||
OnErrorAction.TERMINATE
|
||||
}
|
||||
try {
|
||||
for (src in walkTopDown().fail { f, e -> if (onError(f, e) == OnErrorAction.TERMINATE) throw TerminateException(f) }) {
|
||||
if (!src.exists()) {
|
||||
if (onError(src, NoSuchFileException(file = src, reason = "The source file doesn't exist")) ==
|
||||
OnErrorAction.TERMINATE)
|
||||
return false
|
||||
} else {
|
||||
val relPath = src.relativeTo(this)
|
||||
val dstFile = File(dst, relPath)
|
||||
if (dstFile.exists() && !(src.isDirectory() && dstFile.isDirectory())) {
|
||||
if (onError(dstFile, FileAlreadyExistsException(file = src,
|
||||
other = dstFile,
|
||||
reason = "The destination file already exists")) == OnErrorAction.TERMINATE)
|
||||
return false
|
||||
} else if (src.isDirectory()) {
|
||||
dstFile.mkdirs()
|
||||
} else {
|
||||
if (src.copyTo(dstFile, true) != src.length()) {
|
||||
if (onError(src, IOException("src.length() != dst.length()")) == OnErrorAction.TERMINATE)
|
||||
return false
|
||||
}
|
||||
}
|
||||
} 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
|
||||
return true
|
||||
} catch (e: TerminateException) {
|
||||
return false
|
||||
}
|
||||
|
||||
val result = copy(this)
|
||||
|
||||
return result != OnErrorAction.TERMINATE
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this file with all its children.
|
||||
* Note that if this operation fails then partial deletion may have taken place.
|
||||
*
|
||||
* @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()
|
||||
var result = exists()
|
||||
walkBottomUp().forEach { if (!it.delete()) result = false }
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an array of files and directories in the directory that match the specified [filter]
|
||||
* Returns 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]
|
||||
@@ -348,22 +332,14 @@ public fun File.iterator(): Iterator<File> = FileIterator(this)
|
||||
* @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 != "")
|
||||
val components = filePathComponents()
|
||||
val otherComponents = o.filePathComponents()
|
||||
if (components.rootName != otherComponents.rootName && otherComponents.rootName != "")
|
||||
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)
|
||||
if (components.size < otherComponents.size)
|
||||
return false
|
||||
// Compare first elements until other ends
|
||||
while (otherIt.hasNext()) {
|
||||
if (it.next() != otherIt.next())
|
||||
for (i in 0..otherComponents.size - 1) {
|
||||
if (components.fileList[i] != otherComponents.fileList[i])
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -388,32 +364,17 @@ public fun File.startsWith(o: String): Boolean = startsWith(File(o))
|
||||
* @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 != "")
|
||||
val components = filePathComponents()
|
||||
val otherComponents = o.filePathComponents()
|
||||
if (components.rootName != otherComponents.rootName && otherComponents.rootName != "")
|
||||
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)
|
||||
val shift = components.size - otherComponents.size
|
||||
if (shift < 0)
|
||||
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()
|
||||
for (i in 0..otherComponents.size - 1) {
|
||||
if (components.fileList[i + shift] != otherComponents.fileList[i])
|
||||
return false
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -434,17 +395,15 @@ public fun File.endsWith(o: String): Boolean = endsWith(File(o))
|
||||
* @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())
|
||||
val components = filePathComponents()
|
||||
val rootName = components.rootName
|
||||
val list: MutableList<String> = components.fileList.filter { it.toString() != "." }.map { it.toString() }.toLinkedList()
|
||||
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)
|
||||
list.remove(dots + first)
|
||||
list.remove(dots + first - 1)
|
||||
} else {
|
||||
first++
|
||||
}
|
||||
@@ -474,10 +433,7 @@ 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)
|
||||
return if (ourName.endsWith(File.separatorChar)) File(ourName + o) else File(ourName + File.separatorChar + o)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -497,11 +453,14 @@ public fun File.resolve(o: String): File = resolve(File(o))
|
||||
* @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
|
||||
val components = filePathComponents()
|
||||
val rootName = components.rootName
|
||||
val parentFile = when (components.size) {
|
||||
0 -> null
|
||||
1 -> File(rootName)
|
||||
else -> File(rootName + components.subPath(0, components.size - 1).path)
|
||||
}
|
||||
return if (parentFile != null) parentFile.resolve(o) else o
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user