1970 lines
65 KiB
Kotlin
1970 lines
65 KiB
Kotlin
/*
|
|
* Copyright 2010 dorkbox, llc
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package dorkbox.util
|
|
|
|
import dorkbox.os.OS
|
|
import mu.KotlinLogging
|
|
import java.io.BufferedInputStream
|
|
import java.io.BufferedReader
|
|
import java.io.BufferedWriter
|
|
import java.io.File
|
|
import java.io.FileInputStream
|
|
import java.io.FileNotFoundException
|
|
import java.io.FileOutputStream
|
|
import java.io.FileReader
|
|
import java.io.FileWriter
|
|
import java.io.IOException
|
|
import java.io.InputStream
|
|
import java.io.PrintWriter
|
|
import java.io.RandomAccessFile
|
|
import java.io.Reader
|
|
import java.nio.file.DirectoryIteratorException
|
|
import java.util.*
|
|
import java.util.zip.*
|
|
|
|
/**
|
|
* File related utilities.
|
|
*
|
|
*
|
|
* Contains code from FilenameUtils.java (normalize + dependencies) - Apache 2.0 License
|
|
* http://commons.apache.org/proper/commons-io/
|
|
* Copyright 2013 ASF
|
|
* Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck,
|
|
* Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper,
|
|
* Jeremias Maerki, Stephen Colebourne
|
|
*/
|
|
@Suppress("unused")
|
|
object FileUtil {
|
|
interface Action {
|
|
fun onLineRead(line: String)
|
|
fun finished()
|
|
}
|
|
|
|
/**
|
|
* Gets the version number.
|
|
*/
|
|
val version = "1.29"
|
|
|
|
private val log = KotlinLogging.logger(FileUtil::class.java.name)
|
|
|
|
private const val DEBUG = false
|
|
|
|
/**
|
|
* The Unix separator character.
|
|
*/
|
|
private const val UNIX_SEPARATOR = '/'
|
|
|
|
/**
|
|
* The Windows separator character.
|
|
*/
|
|
private const val WINDOWS_SEPARATOR = '\\'
|
|
|
|
/**
|
|
* The system separator character.
|
|
*/
|
|
private val SYSTEM_SEPARATOR = File.separatorChar
|
|
|
|
/**
|
|
* The separator character that is the opposite of the system separator.
|
|
*/
|
|
private val OTHER_SEPARATOR = if (OS.isWindows) { UNIX_SEPARATOR } else { WINDOWS_SEPARATOR }
|
|
|
|
var ZIP_HEADER = byteArrayOf('P'.code.toByte(), 'K'.code.toByte(), 0x3.toByte(), 0x4.toByte())
|
|
|
|
fun prepend(file: File, vararg strings: String) {
|
|
// make sure we can write to the file
|
|
file.parentFile?.mkdirs()
|
|
|
|
val contents = LinkedList<String>()
|
|
|
|
for (string in strings) {
|
|
contents.add(string)
|
|
}
|
|
|
|
// have to read ORIGINAL file, since we can't prepend it any other way....
|
|
readOnePerLine(file, contents, false)
|
|
|
|
write(file, contents)
|
|
}
|
|
|
|
fun append(file: File, vararg text: String) {
|
|
// make sure we can write to the file
|
|
file.parentFile?.mkdirs()
|
|
|
|
// wooooo for auto-closable and try-with-resources
|
|
try {
|
|
FileWriter(file, true).use { fw ->
|
|
BufferedWriter(fw).use { bw ->
|
|
PrintWriter(bw).use { out ->
|
|
|
|
for (s in text) {
|
|
out.println(s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (e: IOException) {
|
|
log.error("Error appending text", e)
|
|
}
|
|
}
|
|
|
|
fun write(file: File, vararg text: String) {
|
|
// make sure we can write to the file
|
|
file.parentFile?.mkdirs()
|
|
|
|
// wooooo for auto-closable and try-with-resources
|
|
try {
|
|
FileWriter(file, false).use { fw ->
|
|
BufferedWriter(fw).use { bw ->
|
|
PrintWriter(bw).use { out ->
|
|
|
|
for (s in text) {
|
|
out.println(s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (e: IOException) {
|
|
log.error("Error appending text", e)
|
|
}
|
|
|
|
}
|
|
|
|
fun write(file: File, text: List<String>) {
|
|
// make sure we can write to the file
|
|
file.parentFile?.mkdirs()
|
|
|
|
// wooooo for auto-closable and try-with-resources
|
|
try {
|
|
FileWriter(file, false).use { fw ->
|
|
BufferedWriter(fw).use { bw ->
|
|
PrintWriter(bw).use { out ->
|
|
|
|
for (s in text) {
|
|
out.println(s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (e: IOException) {
|
|
log.error("Error appending text", e)
|
|
}
|
|
|
|
}
|
|
|
|
fun append(file: File, text: List<String>) {
|
|
// make sure we can write to the file
|
|
file.parentFile?.mkdirs()
|
|
|
|
// wooooo for auto-closable and try-with-resources
|
|
try {
|
|
FileWriter(file, true).use { fw ->
|
|
BufferedWriter(fw).use { bw ->
|
|
PrintWriter(bw).use { out ->
|
|
|
|
for (s in text) {
|
|
out.println(s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (e: IOException) {
|
|
log.error("Error appending text", e)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts the content of a file into a list of strings. Lines are trimmed.
|
|
*
|
|
* @param file the input file to read. Throws an error if this file cannot be read.
|
|
* @param includeEmptyLines true if you want the resulting list of String to include blank/empty lines from the file
|
|
*
|
|
* @return A list of strings, one line per string, of the content
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun read(file: File, includeEmptyLines: Boolean): List<String> {
|
|
val lines: MutableList<String> = ArrayList()
|
|
|
|
if (includeEmptyLines) {
|
|
file.reader().use {
|
|
it.forEachLine { line ->
|
|
lines.add(line)
|
|
}
|
|
}
|
|
} else {
|
|
file.reader().use {
|
|
it.forEachLine { line ->
|
|
if (line.isNotEmpty()) {
|
|
lines.add(line)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return lines
|
|
}
|
|
|
|
/**
|
|
* Convenience method that converts the content of a file into a giant string.
|
|
*
|
|
* @param file the input file to read. Throws an error if this file cannot be read.
|
|
*
|
|
* @return A string, matching the contents of the file
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun readAsString(file: File): String {
|
|
return file.readText()
|
|
}
|
|
|
|
/**
|
|
* @return contents of the file if we could read the file without errors. Null if we could not
|
|
*/
|
|
fun read(file: String): String? {
|
|
return read(File(file))
|
|
}
|
|
|
|
/**
|
|
* @return contents of the file if we could read the file without errors. Null if we could not
|
|
*/
|
|
fun read(file: File): String? {
|
|
val text = file.readText()
|
|
|
|
if (text.isEmpty()) {
|
|
return null
|
|
}
|
|
|
|
return text
|
|
}
|
|
|
|
/**
|
|
* Reads the content of a file to the passed in StringBuilder.
|
|
*
|
|
* @param file the input file to read. Throws an error if this file cannot be read.
|
|
* @param stringBuilder the stringBuilder this file will be written to
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun read(file: File, stringBuilder: StringBuilder) {
|
|
FileReader(file).use {
|
|
val bin = BufferedReader(it)
|
|
var line: String?
|
|
while (bin.readLine().also { line = it } != null) {
|
|
stringBuilder.append(line).append(OS.LINE_SEPARATOR)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads the content of a file to the passed in StringBuilder.
|
|
*
|
|
* @return true if we could read the file without errors. False if there were errors.
|
|
*/
|
|
fun read(file: File, builder: StringBuilder, lineSeparator: String?): Boolean {
|
|
if (!file.canRead()) {
|
|
return false
|
|
}
|
|
|
|
try {
|
|
file.reader().use { reader ->
|
|
reader.forEachLine { line ->
|
|
if (lineSeparator != null) {
|
|
builder.append(line).append(lineSeparator)
|
|
}
|
|
else {
|
|
builder.append(line)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (ignored: Exception) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Reads each line in a file, performing ACTION for each line.
|
|
*
|
|
* @return true if we could read the file without errors. False if there were errors.
|
|
*/
|
|
fun read(file: File, action: Action): Boolean {
|
|
if (!file.canRead()) {
|
|
return false
|
|
}
|
|
|
|
try {
|
|
file.reader().use { reader ->
|
|
reader.forEachLine { line ->
|
|
action.onLineRead(line)
|
|
}
|
|
}
|
|
}
|
|
catch (ignored: Exception) {
|
|
return false
|
|
}
|
|
action.finished()
|
|
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Will always return a String.
|
|
*
|
|
* @param file the file to read
|
|
*
|
|
* @return the first line in the file, excluding the "new line" character.
|
|
*/
|
|
fun readFirstLine(file: File): String {
|
|
if (!file.canRead()) {
|
|
return ""
|
|
}
|
|
|
|
return file.reader().use { reader ->
|
|
reader.buffered().lineSequence().firstOrNull()
|
|
} ?: ""
|
|
}
|
|
|
|
fun getPid(pidFileName: String): String? {
|
|
val stringBuilder = StringBuilder()
|
|
return if (read(File(pidFileName), stringBuilder, null)) {
|
|
stringBuilder.toString()
|
|
}
|
|
else {
|
|
null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads the contents of the supplied input stream into a list of lines.
|
|
*
|
|
* @return Always returns a list, even if the file does not exist, or there are errors reading it.
|
|
*/
|
|
fun readLines(file: File): List<String> {
|
|
val fileReader = try {
|
|
FileReader(file)
|
|
} catch (ignored: FileNotFoundException) {
|
|
return ArrayList()
|
|
}
|
|
return readLines(fileReader)
|
|
}
|
|
|
|
/**
|
|
* Reads the contents of the supplied input stream into a list of lines.
|
|
*
|
|
*
|
|
* Closes the reader on successful or failed completion.
|
|
*
|
|
* @return Always returns a list, even if the file does not exist, or there are errors reading it.
|
|
*/
|
|
fun readLines(`in`: Reader): List<String> {
|
|
val lines: MutableList<String> = ArrayList()
|
|
|
|
BufferedReader(`in`).use {
|
|
val bin = BufferedReader(`in`)
|
|
var line: String
|
|
try {
|
|
while (bin.readLine().also { line = it } != null) {
|
|
lines.add(line)
|
|
}
|
|
} catch (ignored: IOException) {
|
|
}
|
|
}
|
|
|
|
return lines
|
|
}
|
|
|
|
/**
|
|
* @return a list of the contents of a file, one line at a time. Ignores lines that start with #
|
|
*/
|
|
fun readOnePerLine(file: File): ArrayList<String> {
|
|
val list = ArrayList<String>()
|
|
readOnePerLine(file, list, true)
|
|
|
|
return list
|
|
}
|
|
|
|
/**
|
|
* @return a list of the contents of a file, one line at a time. Ignores lines that start with #
|
|
*/
|
|
fun readOnePerLine(file: File, trimStrings: Boolean): ArrayList<String> {
|
|
val list = ArrayList<String>()
|
|
readOnePerLine(file, list, trimStrings)
|
|
|
|
return list
|
|
}
|
|
|
|
/**
|
|
* @return a list of the contents of a file, one line at a time. Ignores lines that start with #
|
|
*/
|
|
fun readOnePerLine(file: File, list: MutableList<String>, trimStrings: Boolean) {
|
|
if (trimStrings) {
|
|
read(file, object : Action {
|
|
var lineNumber = 0
|
|
|
|
override fun onLineRead(line: String) {
|
|
if (line.isNotEmpty() && !line.startsWith("#")) {
|
|
val newLine = line.trim()
|
|
|
|
if (newLine.isNotEmpty()) {
|
|
list.add(newLine)
|
|
}
|
|
}
|
|
|
|
lineNumber++
|
|
}
|
|
|
|
override fun finished() {}
|
|
})
|
|
}
|
|
else {
|
|
read(file, object : Action {
|
|
var lineNumber = 0
|
|
|
|
override fun onLineRead(line: String) {
|
|
list.add(line)
|
|
lineNumber++
|
|
}
|
|
|
|
override fun finished() {}
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return true if the directory was fully deleted. A false indicates that a partial delete has occurred
|
|
*/
|
|
fun deleteDirectory(dir: File): Boolean {
|
|
try {
|
|
return dir.deleteRecursively()
|
|
}
|
|
catch (e: IOException) {
|
|
log.error("Error deleting the contents of dir $dir", e)
|
|
}
|
|
catch (e: DirectoryIteratorException) {
|
|
log.error("Error deleting the contents of dir $dir", e)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Renames a file. Windows has all sorts of problems which are worked around.
|
|
*
|
|
* @return true if successful, false otherwise
|
|
*/
|
|
fun renameTo(source: File, dest: File): Boolean {
|
|
// if we're on a civilized operating system we may be able to simple
|
|
// rename it
|
|
if (source.renameTo(dest)) {
|
|
return true
|
|
}
|
|
|
|
// fall back to trying to rename the old file out of the way, rename the
|
|
// new file into
|
|
// place and then delete the old file
|
|
if (dest.exists()) {
|
|
val temp = File(dest.path + "_old")
|
|
if (temp.exists()) {
|
|
if (!temp.delete()) {
|
|
if (DEBUG) {
|
|
System.err.println("Failed to delete old intermediate file: $temp")
|
|
}
|
|
|
|
// the subsequent code will probably fail
|
|
}
|
|
}
|
|
if (dest.renameTo(temp)) {
|
|
if (source.renameTo(dest)) {
|
|
if (temp.delete()) {
|
|
if (DEBUG) {
|
|
System.err.println("Failed to delete intermediate file: $temp")
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
// as a last resort, try copying the old data over the new
|
|
return try {
|
|
source.copyTo(dest)
|
|
if (!source.delete()) {
|
|
if (DEBUG) {
|
|
System.err.println("Failed to delete '$source' after brute force copy to '$dest'.")
|
|
}
|
|
}
|
|
true
|
|
} catch (ioe: IOException) {
|
|
if (DEBUG) {
|
|
System.err.println("Failed to copy '$source' to '$dest'.")
|
|
ioe.printStackTrace()
|
|
}
|
|
false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copies a files from one location to another. Overwriting any existing file at the destination.
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun copyFile(`in`: String, out: File): File {
|
|
return copyFile(File(`in`), out)
|
|
}
|
|
|
|
/**
|
|
* Copies a files from one location to another. Overwriting any existing file at the destination.
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun copyFile(`in`: File, out: String): File {
|
|
return copyFile(`in`, File(out))
|
|
}
|
|
|
|
/**
|
|
* Copies a files from one location to another. Overwriting any existing file at the destination.
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun copyFile(`in`: String, out: String): File {
|
|
return copyFile(File(`in`), File(out))
|
|
}
|
|
|
|
/**
|
|
* Copies a files from one location to another. Overwriting any existing file at the destination.
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun copyFileToDir(`in`: String, out: String): File {
|
|
return copyFileToDir(File(`in`), File(out))
|
|
}
|
|
|
|
/**
|
|
* Copies a files from one location to another. Overwriting any existing file at the destination.
|
|
* If the out file is a directory, then the in file will be copied to the directory
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun copyFileToDir(`in`: File, out: File): File {
|
|
// copy the file to the directory instead
|
|
if (!out.isDirectory) {
|
|
throw IOException("Out file is not a directory! '" + out.absolutePath + "'")
|
|
}
|
|
return copyFile(`in`, File(out, `in`.name))
|
|
}
|
|
|
|
/**
|
|
* Copies a files from one location to another. Overwriting any existing file at the destination.
|
|
*/
|
|
@JvmStatic
|
|
@Throws(IOException::class)
|
|
fun copyFile(`in`: File, out: File): File {
|
|
val normalizedIn = normalize(`in`)!!.absolutePath
|
|
val normalizedout = normalize(out)!!.absolutePath
|
|
if (normalizedIn.equals(normalizedout, ignoreCase = true)) {
|
|
if (DEBUG) {
|
|
System.err.println("Source equals destination! $normalizedIn")
|
|
}
|
|
return out
|
|
}
|
|
|
|
|
|
// if out doesn't exist, then create it.
|
|
val parentOut: File? = out.parentFile
|
|
if (parentOut?.canWrite() == false) {
|
|
parentOut.mkdirs()
|
|
}
|
|
if (DEBUG) {
|
|
System.err.println("Copying file: '$`in`' --> '$out'")
|
|
}
|
|
|
|
`in`.copyTo(`out`)
|
|
out.setLastModified(`in`.lastModified())
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* Copies the contents of file two onto the END of file one.
|
|
*/
|
|
fun concatFiles(one: File, two: File): File {
|
|
if (DEBUG) {
|
|
System.err.println("Concat'ing file: '$one' --> '$two'")
|
|
}
|
|
|
|
one.appendBytes(two.readBytes())
|
|
|
|
one.setLastModified(System.currentTimeMillis())
|
|
return one
|
|
}
|
|
|
|
/**
|
|
* Moves a file, overwriting any existing file at the destination.
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun moveFile(`in`: String, out: File): File {
|
|
return moveFile(File(`in`), out)
|
|
}
|
|
|
|
/**
|
|
* Moves a file, overwriting any existing file at the destination.
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun moveFile(`in`: File, out: String): File {
|
|
return moveFile(`in`, File(out))
|
|
}
|
|
|
|
/**
|
|
* Moves a file, overwriting any existing file at the destination.
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun moveFile(`in`: String, out: String): File {
|
|
return moveFile(File(`in`), File(out))
|
|
}
|
|
|
|
/**
|
|
* Moves a file, overwriting any existing file at the destination.
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun moveFile(`in`: File, out: File): File {
|
|
if (out.canRead()) {
|
|
out.delete()
|
|
}
|
|
val renameSuccess = renameTo(`in`, out)
|
|
if (!renameSuccess) {
|
|
throw IOException("Unable to move file: '" + `in`.absolutePath + "' -> '" + out.absolutePath + "'")
|
|
}
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* Copies a directory from one location to another
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun copyDirectory(src: String, dest: String, vararg namesToIgnore: String) {
|
|
copyDirectory(File(src), File(dest), *namesToIgnore)
|
|
}
|
|
|
|
/**
|
|
* Copies a directory from one location to another
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun copyDirectory(src_: File, dest_: File, vararg namesToIgnore: String) {
|
|
val src = normalize(src_)
|
|
val dest = normalize(dest_)
|
|
|
|
requireNotNull(src) { "Source must be valid" }
|
|
requireNotNull(dest) { "Destination must be valid" }
|
|
|
|
if (namesToIgnore.isNotEmpty()) {
|
|
val name = src.name
|
|
for (ignore in namesToIgnore) {
|
|
if (name == ignore) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
if (src.isDirectory) {
|
|
// if directory not exists, create it
|
|
if (!dest.exists()) {
|
|
dest.mkdir()
|
|
if (DEBUG) {
|
|
System.err.println("Directory copied from '$src' --> '$dest'")
|
|
}
|
|
}
|
|
|
|
// list all the directory contents
|
|
val files = src.list()
|
|
if (files != null) {
|
|
for (file in files) {
|
|
// construct the src and dest file structure
|
|
val srcFile = File(src, file)
|
|
val destFile = File(dest, file)
|
|
|
|
// recursive copy
|
|
copyDirectory(srcFile, destFile, *namesToIgnore)
|
|
}
|
|
}
|
|
} else {
|
|
// if file, then copy it
|
|
copyFile(src, dest)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Safely moves a directory from one location to another (by copying it first, then deleting the original).
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun moveDirectory(src: String, dest: String, vararg fileNamesToIgnore: String) {
|
|
moveDirectory(File(src), File(dest), *fileNamesToIgnore)
|
|
}
|
|
|
|
/**
|
|
* Safely moves a directory from one location to another (by copying it first, then deleting the original).
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun moveDirectory(src: File, dest: File, vararg fileNamesToIgnore: String) {
|
|
if (fileNamesToIgnore.size > 0) {
|
|
val name = src.name
|
|
for (ignore in fileNamesToIgnore) {
|
|
if (name == ignore) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
if (src.isDirectory) {
|
|
// if directory not exists, create it
|
|
if (!dest.exists()) {
|
|
dest.mkdir()
|
|
if (DEBUG) {
|
|
System.err.println("Directory copied from '$src' --> '$dest'")
|
|
}
|
|
}
|
|
|
|
// list all the directory contents
|
|
val files = src.list()
|
|
if (files != null) {
|
|
for (file in files) {
|
|
// construct the src and dest file structure
|
|
val srcFile = File(src, file)
|
|
val destFile = File(dest, file)
|
|
|
|
// recursive copy
|
|
moveDirectory(srcFile, destFile, *fileNamesToIgnore)
|
|
}
|
|
}
|
|
} else {
|
|
// if file, then copy it
|
|
moveFile(src, dest)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes a file or directory and all files and sub-directories under it.
|
|
*
|
|
* @param fileNamesToIgnore if prefaced with a '/', it will ignore as a directory instead of file
|
|
* @return true iff the file/dir was deleted
|
|
*/
|
|
fun delete(fileName: String, vararg fileNamesToIgnore: String): Boolean {
|
|
return delete(File(fileName), *fileNamesToIgnore)
|
|
}
|
|
|
|
/**
|
|
* Deletes a file, directory + all files and sub-directories under it. The directory is ALSO deleted if it because empty as a result
|
|
* of this operation
|
|
*
|
|
* @param namesToIgnore if prefaced with a '/', it will treat the name to ignore as a directory instead of file
|
|
*
|
|
* @return true IFF the file/dir was deleted or didn't exist at first
|
|
*/
|
|
@JvmStatic
|
|
fun delete(file: File, vararg namesToIgnore: String): Boolean {
|
|
if (!file.exists()) {
|
|
return true
|
|
}
|
|
var thingsDeleted = false
|
|
var ignored = false
|
|
if (file.isDirectory) {
|
|
val files = file.listFiles()
|
|
if (files != null) {
|
|
var i = 0
|
|
val n = files.size
|
|
while (i < n) {
|
|
var delete = true
|
|
val file2 = files[i]
|
|
val name2 = file2.name
|
|
val name2Full = normalize(file2)!!.absolutePath
|
|
if (file2.isDirectory) {
|
|
for (name in namesToIgnore) {
|
|
if (name[0] == UNIX_SEPARATOR && name == name2) {
|
|
// only name match if our name To Ignore starts with a / or \
|
|
if (DEBUG) {
|
|
System.err.println("Skipping delete dir: $file2")
|
|
}
|
|
ignored = true
|
|
delete = false
|
|
break
|
|
} else if (name == name2Full) {
|
|
// full path match
|
|
if (DEBUG) {
|
|
System.err.println("Skipping delete dir: $file2")
|
|
}
|
|
ignored = true
|
|
delete = false
|
|
break
|
|
}
|
|
}
|
|
if (delete) {
|
|
if (DEBUG) {
|
|
System.err.println("Deleting dir: $file2")
|
|
}
|
|
delete(file2, *namesToIgnore)
|
|
}
|
|
} else {
|
|
for (name in namesToIgnore) {
|
|
if (name[0] != UNIX_SEPARATOR && name == name2) {
|
|
// only name match
|
|
if (DEBUG) {
|
|
System.err.println("Skipping delete file: $file2")
|
|
}
|
|
ignored = true
|
|
delete = false
|
|
break
|
|
} else if (name == name2Full) {
|
|
// full path match
|
|
if (DEBUG) {
|
|
System.err.println("Skipping delete file: $file2")
|
|
}
|
|
ignored = true
|
|
delete = false
|
|
break
|
|
}
|
|
}
|
|
if (delete) {
|
|
if (DEBUG) {
|
|
System.err.println("Deleting file: $file2")
|
|
}
|
|
thingsDeleted = thingsDeleted or file2.delete()
|
|
}
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
}
|
|
|
|
// don't try to delete the dir if there was an ignored file in it
|
|
if (ignored) {
|
|
if (DEBUG) {
|
|
System.err.println("Skipping deleting file: $file")
|
|
}
|
|
return false
|
|
}
|
|
if (DEBUG) {
|
|
System.err.println("Deleting file: $file")
|
|
}
|
|
thingsDeleted = thingsDeleted or file.delete()
|
|
return thingsDeleted
|
|
}
|
|
|
|
/**
|
|
* @return the contents of the file as a byte array
|
|
*/
|
|
fun toBytes(file: File): ByteArray? {
|
|
return file.readBytes()
|
|
}
|
|
|
|
/**
|
|
* Creates the directories in the specified location.
|
|
*/
|
|
fun mkdir(location: File): String {
|
|
val path = normalize(location)!!.absoluteFile
|
|
if (location.mkdirs()) {
|
|
if (DEBUG) {
|
|
System.err.println("Created directory: $path")
|
|
}
|
|
}
|
|
return path.path
|
|
}
|
|
|
|
/**
|
|
* Creates the directories in the specified location.
|
|
*/
|
|
fun mkdir(location: String): String {
|
|
return mkdir(File(location))
|
|
}
|
|
|
|
/**
|
|
* Creates a temp file
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun tempFile(fileName: String): File {
|
|
return normalize(File.createTempFile(fileName, null))!!.absoluteFile
|
|
}
|
|
|
|
/**
|
|
* Creates a temp directory
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun tempDirectory(directoryName: String): String {
|
|
val file = File.createTempFile(directoryName, null)
|
|
if (!file.delete()) {
|
|
throw IOException("Unable to delete temp file: $file")
|
|
}
|
|
if (!file.mkdir()) {
|
|
throw IOException("Unable to create temp directory: $file")
|
|
}
|
|
return normalize(file)!!.absolutePath
|
|
}
|
|
|
|
/**
|
|
* @return true if the inputStream is a zip/jar stream. DOES NOT CLOSE THE STREAM
|
|
*/
|
|
fun isZipStream(`in`: InputStream): Boolean {
|
|
var `in` = `in`
|
|
if (!`in`.markSupported()) {
|
|
`in` = BufferedInputStream(`in`)
|
|
}
|
|
var isZip = true
|
|
try {
|
|
`in`.mark(ZIP_HEADER.size)
|
|
for (i in ZIP_HEADER.indices) {
|
|
if (ZIP_HEADER[i] != `in`.read().toByte()) {
|
|
isZip = false
|
|
break
|
|
}
|
|
}
|
|
`in`.reset()
|
|
} catch (e: Exception) {
|
|
isZip = false
|
|
}
|
|
return isZip
|
|
}
|
|
|
|
/**
|
|
* @return true if the named file is a zip/jar file
|
|
*/
|
|
fun isZipFile(fileName: String): Boolean {
|
|
return isZipFile(File(fileName))
|
|
}
|
|
|
|
/**
|
|
* @return true if the file is a zip/jar file
|
|
*/
|
|
fun isZipFile(file: File): Boolean {
|
|
var isZip = true
|
|
val buffer = ByteArray(ZIP_HEADER.size)
|
|
var raf: RandomAccessFile? = null
|
|
try {
|
|
raf = RandomAccessFile(file, "r")
|
|
raf.readFully(buffer)
|
|
for (i in ZIP_HEADER.indices) {
|
|
if (buffer[i] != ZIP_HEADER[i]) {
|
|
isZip = false
|
|
break
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
isZip = false
|
|
(e as? FileNotFoundException)?.printStackTrace()
|
|
} finally {
|
|
if (raf != null) {
|
|
try {
|
|
raf.close()
|
|
} catch (e: IOException) {
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
}
|
|
return isZip
|
|
}
|
|
|
|
/**
|
|
* Unzips a ZIP file
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun unzip(zipFile: String, outputDir: String) {
|
|
unzipJar(zipFile, outputDir, true)
|
|
}
|
|
|
|
/**
|
|
* Unzips a ZIP file
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun unzip(zipFile: File, outputDir: File) {
|
|
unzipJar(zipFile, outputDir, true)
|
|
}
|
|
|
|
/**
|
|
* Unzips a ZIP file. Will close the input stream.
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun unzip(inputStream: ZipInputStream, outputDir: String) {
|
|
unzip(inputStream, File(outputDir))
|
|
}
|
|
|
|
/**
|
|
* Unzips a ZIP file. Will close the input stream.
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun unzip(inputStream: ZipInputStream, outputDir: File) {
|
|
unzipJar(inputStream, outputDir, true)
|
|
}
|
|
|
|
/**
|
|
* Unzips a ZIP file
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun unzipJar(zipFile: String, outputDir: String, extractManifest: Boolean) {
|
|
unjarzip0(File(zipFile), File(outputDir), extractManifest)
|
|
}
|
|
|
|
/**
|
|
* Unzips a ZIP file
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun unzipJar(zipFile: File, outputDir: File, extractManifest: Boolean) {
|
|
unjarzip0(zipFile, outputDir, extractManifest)
|
|
}
|
|
|
|
/**
|
|
* Unzips a ZIP file. Will close the input stream.
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun unzipJar(inputStream: ZipInputStream, outputDir: File, extractManifest: Boolean) {
|
|
unjarzip1(inputStream, outputDir, extractManifest)
|
|
}
|
|
|
|
/**
|
|
* Unzips a ZIP or JAR file (and handles the manifest if requested)
|
|
*/
|
|
@Throws(IOException::class)
|
|
private fun unjarzip0(zipFile: File, outputDir: File, extractManifest: Boolean) {
|
|
val fileLength = zipFile.length()
|
|
if (fileLength > Int.MAX_VALUE - 1) {
|
|
throw RuntimeException("Source filesize is too large!")
|
|
}
|
|
val inputStream = ZipInputStream(FileInputStream(zipFile))
|
|
unjarzip1(inputStream, outputDir, extractManifest)
|
|
}
|
|
|
|
/**
|
|
* Unzips a ZIP file
|
|
*/
|
|
@Throws(IOException::class)
|
|
private fun unjarzip1(inputStream: ZipInputStream, outputDir: File, extractManifest: Boolean) {
|
|
|
|
inputStream.use {
|
|
var entry: ZipEntry
|
|
while (inputStream.nextEntry.also { entry = it } != null) {
|
|
val name = entry.name
|
|
if (!extractManifest && name.startsWith("META-INF/")) {
|
|
continue
|
|
}
|
|
val file = File(outputDir, name)
|
|
if (entry.isDirectory) {
|
|
mkdir(file.path)
|
|
continue
|
|
}
|
|
mkdir(file.parent)
|
|
|
|
FileOutputStream(file).use {
|
|
inputStream.copyTo(it)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses the specified root directory for **ALL** files that are in it. All of the sub-directories are searched as well.
|
|
*
|
|
*
|
|
* *This is different, in that it returns ALL FILES, instead of ones that just match a specific extension.*
|
|
*
|
|
* @return the list of all files in the root+sub-dirs.
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun parseDir(rootDirectory: String): List<File> {
|
|
return parseDir(File(rootDirectory))
|
|
}
|
|
|
|
/**
|
|
* Parses the specified root directory for **ALL** files that are in it. All of the sub-directories are searched as well.
|
|
*
|
|
*
|
|
* *This is different, in that it returns ALL FILES, instead of ones that just match a specific extension.*
|
|
*
|
|
* @return the list of all files in the root+sub-dirs.
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun parseDir(rootDirectory: File): List<File> {
|
|
return parseDir(rootDirectory)
|
|
}
|
|
|
|
/**
|
|
* Parses the specified root directory for files that end in the extension to match. All of the sub-directories are searched as well.
|
|
*
|
|
* @return the list of all files in the root+sub-dirs that match the given extension.
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun parseDir(rootDirectory: File, vararg extensionsToMatch: String): List<File> {
|
|
val jarList: MutableList<File> = LinkedList()
|
|
val directories = LinkedList<File?>()
|
|
|
|
val rootDirectory = normalize(rootDirectory) ?: throw IOException("Root directory was invalid!")
|
|
|
|
if (!rootDirectory.exists()) {
|
|
throw IOException("Location does not exist: " + rootDirectory.absolutePath)
|
|
}
|
|
|
|
if (rootDirectory.isDirectory) {
|
|
directories.add(rootDirectory)
|
|
while (directories.peek() != null) {
|
|
val dir = directories.poll()
|
|
val listFiles = dir!!.listFiles()
|
|
if (listFiles != null) {
|
|
for (file in listFiles) {
|
|
if (file.isDirectory) {
|
|
directories.add(file)
|
|
} else {
|
|
if (extensionsToMatch.isEmpty()) {
|
|
jarList.add(file)
|
|
} else {
|
|
for (e in extensionsToMatch) {
|
|
if (file.absolutePath.endsWith(e)) {
|
|
jarList.add(file)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
throw IOException("Cannot search directory children if the dir is a file name: " + rootDirectory.absolutePath)
|
|
}
|
|
return jarList
|
|
}
|
|
|
|
/**
|
|
* Gets the relative path of a file to a specific directory in it's hierarchy.
|
|
*
|
|
*
|
|
* For example: getChildRelativeToDir("/a/b/c/d/e.bah", "c") -> "d/e.bah"
|
|
*
|
|
* @return null if there is no child
|
|
*/
|
|
fun getChildRelativeToDir(fileName: String, dirInHeirarchy: String): String? {
|
|
require(fileName.isEmpty()) { "fileName cannot be empty." }
|
|
return getChildRelativeToDir(File(fileName), dirInHeirarchy)
|
|
}
|
|
|
|
/**
|
|
* Gets the relative path of a file to a specific directory in it's hierarchy.
|
|
*
|
|
*
|
|
* For example: getChildRelativeToDir("/a/b/c/d/e.bah", "c") -> "d/e.bah"
|
|
*
|
|
* @return null if there is no child
|
|
*/
|
|
fun getChildRelativeToDir(file: File, dirInHeirarchy: String): String? {
|
|
require(dirInHeirarchy.isEmpty()) { "dirInHeirarchy cannot be empty." }
|
|
|
|
val split = dirInHeirarchy.split(File.separator).toTypedArray()
|
|
var splitIndex = split.size - 1
|
|
val absolutePath = file.absolutePath
|
|
var parent: File? = file
|
|
var parentName: String
|
|
|
|
if (splitIndex == 0) {
|
|
// match on ONE dir
|
|
while (parent != null) {
|
|
parentName = parent.name
|
|
if (parentName == dirInHeirarchy) {
|
|
parentName = parent.absolutePath
|
|
return absolutePath.substring(parentName.length + 1)
|
|
}
|
|
parent = parent.parentFile
|
|
}
|
|
} else {
|
|
// match on MANY dir. They must be "in-order"
|
|
var matched = false
|
|
while (parent != null) {
|
|
parentName = parent.name
|
|
if (matched) {
|
|
if (parentName == split[splitIndex]) {
|
|
splitIndex--
|
|
if (splitIndex < 0) {
|
|
// this means the ENTIRE path matched
|
|
return if (absolutePath.length == dirInHeirarchy.length) {
|
|
null
|
|
} else absolutePath.substring(dirInHeirarchy.length + 1, absolutePath.length)
|
|
|
|
// +1 to account for the separator char
|
|
}
|
|
} else {
|
|
// because it has to be "in-order", if it doesn't match, we immediately abort
|
|
return null
|
|
}
|
|
} else {
|
|
if (parentName == split[splitIndex]) {
|
|
matched = true
|
|
splitIndex--
|
|
}
|
|
}
|
|
parent = parent.parentFile
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Gets the PARENT relative path of a file to a specific directory in it's hierarchy.
|
|
*
|
|
*
|
|
* For example: getParentRelativeToDir("/a/b/c/d/e.bah", "c") -> "/a/b"
|
|
*/
|
|
fun getParentRelativeToDir(fileName: String, dirInHeirarchy: String): String? {
|
|
require(fileName.isEmpty()) { "fileName cannot be empty." }
|
|
|
|
return getParentRelativeToDir(File(fileName), dirInHeirarchy)
|
|
}
|
|
|
|
/**
|
|
* Gets the relative path of a file to a specific directory in it's hierarchy.
|
|
*
|
|
*
|
|
* For example: getParentRelativeToDir("/a/b/c/d/e.bah", "c") -> "/a/b"
|
|
*
|
|
* @return null if it cannot be found
|
|
*/
|
|
fun getParentRelativeToDir(file: File, dirInHeirarchy: String): String? {
|
|
require(dirInHeirarchy.isEmpty()) { "dirInHeirarchy cannot be empty." }
|
|
|
|
val split = dirInHeirarchy.split(File.separator).toTypedArray()
|
|
var splitIndex = split.size - 1
|
|
var parent = file
|
|
var parentName: String
|
|
if (splitIndex == 0) {
|
|
// match on ONE dir
|
|
while (parent != null) {
|
|
parentName = parent.name
|
|
if (parentName == dirInHeirarchy) {
|
|
parent = parent.parentFile
|
|
parentName = parent.absolutePath
|
|
return parentName
|
|
}
|
|
parent = parent.parentFile
|
|
}
|
|
} else {
|
|
// match on MANY dir. They must be "in-order"
|
|
var matched = false
|
|
while (parent != null) {
|
|
parentName = parent.name
|
|
if (matched) {
|
|
if (parentName == split[splitIndex]) {
|
|
splitIndex--
|
|
if (splitIndex < 0) {
|
|
parent = parent.parentFile
|
|
parentName = parent.absolutePath
|
|
return parentName
|
|
}
|
|
} else {
|
|
// because it has to be "in-order", if it doesn't match, we immediately abort
|
|
return null
|
|
}
|
|
} else {
|
|
if (parentName == split[splitIndex]) {
|
|
matched = true
|
|
splitIndex--
|
|
}
|
|
}
|
|
parent = parent.parentFile
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Extracts a file from a zip into a TEMP file, if possible. The TEMP file is deleted upon JVM exit.
|
|
*
|
|
* @return the location of the extracted file, or NULL if the file cannot be extracted or doesn't exist.
|
|
*/
|
|
@Throws(IOException::class)
|
|
fun extractFromZip(zipFile: String, fileToExtract: String): String? {
|
|
ZipInputStream(FileInputStream(zipFile)).use { inputStream ->
|
|
while (true) {
|
|
val entry = inputStream.nextEntry ?: break
|
|
val name = entry.name
|
|
if (entry.isDirectory) {
|
|
continue
|
|
}
|
|
|
|
if (name == fileToExtract) {
|
|
val tempFile = tempFile(name)
|
|
tempFile.deleteOnExit()
|
|
val output = FileOutputStream(tempFile)
|
|
output.use { output ->
|
|
inputStream.copyTo(output)
|
|
}
|
|
return tempFile.absolutePath
|
|
}
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Touches a file, so that it's timestamp is right now. If the file is not created, it will be created automatically.
|
|
*
|
|
* @return true if the touch succeeded, false otherwise
|
|
*/
|
|
fun touch(file: String): Boolean {
|
|
val timestamp = System.currentTimeMillis()
|
|
return touch(File(file).absoluteFile, timestamp)
|
|
}
|
|
|
|
/**
|
|
* Touches a file, so that it's timestamp is right now. If the file is not created, it will be created automatically.
|
|
*
|
|
* @return true if the touch succeeded, false otherwise
|
|
*/
|
|
fun touch(file: File): Boolean {
|
|
val timestamp = System.currentTimeMillis()
|
|
return touch(file, timestamp)
|
|
}
|
|
|
|
/**
|
|
* Touches a file, so that it's timestamp is right now. If the file is not created, it will be created automatically.
|
|
*
|
|
* @return true if the touch succeeded, false otherwise
|
|
*/
|
|
fun touch(file: File, timestamp: Long): Boolean {
|
|
if (!file.exists()) {
|
|
val mkdirs = file.parentFile.mkdirs()
|
|
if (!mkdirs) {
|
|
// error creating the parent directories.
|
|
return false
|
|
}
|
|
try {
|
|
FileOutputStream(file).close()
|
|
} catch (ignored: IOException) {
|
|
return false
|
|
}
|
|
}
|
|
return file.setLastModified(timestamp)
|
|
}
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------
|
|
/*
|
|
* FilenameUtils.java (normalize + dependencies) - Apache 2.0 License
|
|
* http://commons.apache.org/proper/commons-io/
|
|
* Copyright 2013 ASF
|
|
* Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck,
|
|
* Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper,
|
|
* Jeremias Maerki, Stephen Colebourne
|
|
*/
|
|
/**
|
|
* Normalizes a path, removing double and single dot path steps.
|
|
*
|
|
*
|
|
* THIS IS DIFFERENT in that it might not be a path that resolves to anything
|
|
*
|
|
*
|
|
* This method normalizes a path to a standard format.
|
|
* The input may contain separators in either Unix or Windows format.
|
|
* The output will contain separators in the format of the system.
|
|
*
|
|
*
|
|
* A trailing slash will be retained.
|
|
* A double slash will be merged to a single slash (but UNC names are handled).
|
|
* A single dot path segment will be removed.
|
|
* A double dot will cause that path segment and the one before to be removed.
|
|
* If the double dot has no parent path segment to work with, `null`
|
|
* is returned.
|
|
*
|
|
*
|
|
* The output will be the same on both Unix and Windows except
|
|
* for the separator character.
|
|
* <pre>
|
|
* /foo// --> /foo/
|
|
* /foo/./ --> /foo/
|
|
* /foo/../bar --> /bar
|
|
* /foo/../bar/ --> /bar/
|
|
* /foo/../bar/../baz --> /baz
|
|
* //foo//./bar --> /foo/bar
|
|
* /../ --> null
|
|
* ../foo --> null
|
|
* foo/bar/.. --> foo/
|
|
* foo/../../bar --> null
|
|
* foo/../bar --> bar
|
|
* //server/foo/../bar --> //server/bar
|
|
* //server/../bar --> null
|
|
* C:\foo\..\bar --> C:\bar
|
|
* C:\..\bar --> null
|
|
* ~/foo/../bar/ --> ~/bar/
|
|
* ~/../bar --> null
|
|
</pre> *
|
|
* (Note the file separator returned will be correct for Windows/Unix)
|
|
*
|
|
* @param filename the filename to normalize, null returns null
|
|
*
|
|
* @return the normalized filename, or null if invalid
|
|
*/
|
|
fun normalizeRaw(filename: String): String? {
|
|
return doNormalize(filename, SYSTEM_SEPARATOR, true)
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* FilenameUtils.java (normalize + dependencies) - Apache 2.0 License
|
|
* http://commons.apache.org/proper/commons-io/
|
|
* Copyright 2013 ASF
|
|
* Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck,
|
|
* Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper,
|
|
* Jeremias Maerki, Stephen Colebourne
|
|
*/
|
|
/**
|
|
* Normalizes a path, removing double and single dot path steps.
|
|
*
|
|
*
|
|
* This method normalizes a path to a standard format.
|
|
* The input may contain separators in either Unix or Windows format.
|
|
* The output will contain separators in the format of the system.
|
|
*
|
|
*
|
|
* A trailing slash will be retained.
|
|
* A double slash will be merged to a single slash (but UNC names are handled).
|
|
* A single dot path segment will be removed.
|
|
* A double dot will cause that path segment and the one before to be removed.
|
|
* If the double dot has no parent path segment to work with, `null`
|
|
* is returned.
|
|
*
|
|
*
|
|
* The output will be the same on both Unix and Windows except
|
|
* for the separator character.
|
|
* <pre>
|
|
* /foo// --> /foo/
|
|
* /foo/./ --> /foo/
|
|
* /foo/../bar --> /bar
|
|
* /foo/../bar/ --> /bar/
|
|
* /foo/../bar/../baz --> /baz
|
|
* //foo//./bar --> /foo/bar
|
|
* /../ --> null
|
|
* ../foo --> null
|
|
* foo/bar/.. --> foo/
|
|
* foo/../../bar --> null
|
|
* foo/../bar --> bar
|
|
* //server/foo/../bar --> //server/bar
|
|
* //server/../bar --> null
|
|
* C:\foo\..\bar --> C:\bar
|
|
* C:\..\bar --> null
|
|
* ~/foo/../bar/ --> ~/bar/
|
|
* ~/../bar --> null
|
|
</pre> *
|
|
* (Note the file separator returned will be correct for Windows/Unix)
|
|
*
|
|
* @param filename the file to normalize, null returns null
|
|
* @return the normalized file, or null if invalid
|
|
*/
|
|
fun normalize(filename: String): File? {
|
|
val asString = doNormalize(File(filename).absolutePath, SYSTEM_SEPARATOR, true) ?: return null
|
|
return File(asString).absoluteFile
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* FilenameUtils.java (normalize + dependencies) - Apache 2.0 License
|
|
* http://commons.apache.org/proper/commons-io/
|
|
* Copyright 2013 ASF
|
|
* Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck,
|
|
* Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper,
|
|
* Jeremias Maerki, Stephen Colebourne
|
|
*/
|
|
/**
|
|
* Normalizes a path, removing double and single dot path steps.
|
|
*
|
|
*
|
|
* This method normalizes a path to a standard format.
|
|
* The input may contain separators in either Unix or Windows format.
|
|
* The output will contain separators in the format of the system.
|
|
*
|
|
*
|
|
* A trailing slash will be retained.
|
|
* A double slash will be merged to a single slash (but UNC names are handled).
|
|
* A single dot path segment will be removed.
|
|
* A double dot will cause that path segment and the one before to be removed.
|
|
* If the double dot has no parent path segment to work with, `null`
|
|
* is returned.
|
|
*
|
|
*
|
|
* The output will be the same on both Unix and Windows except
|
|
* for the separator character.
|
|
* <pre>
|
|
* /foo// --> /foo/
|
|
* /foo/./ --> /foo/
|
|
* /foo/../bar --> /bar
|
|
* /foo/../bar/ --> /bar/
|
|
* /foo/../bar/../baz --> /baz
|
|
* //foo//./bar --> /foo/bar
|
|
* /../ --> null
|
|
* ../foo --> null
|
|
* foo/bar/.. --> foo/
|
|
* foo/../../bar --> null
|
|
* foo/../bar --> bar
|
|
* //server/foo/../bar --> //server/bar
|
|
* //server/../bar --> null
|
|
* C:\foo\..\bar --> C:\bar
|
|
* C:\..\bar --> null
|
|
* ~/foo/../bar/ --> ~/bar/
|
|
* ~/../bar --> null
|
|
</pre> *
|
|
* (Note the file separator returned will be correct for Windows/Unix)
|
|
*
|
|
* @param file the file to normalize, null returns null
|
|
* @return the normalized file, or null if invalid
|
|
*/
|
|
fun normalize(file: File): File? {
|
|
val asString = doNormalize(file.absolutePath, SYSTEM_SEPARATOR, true) ?: return null
|
|
return File(asString).absoluteFile
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* FilenameUtils.java (normalize + dependencies) - Apache 2.0 License
|
|
* http://commons.apache.org/proper/commons-io/
|
|
* Copyright 2013 ASF
|
|
* Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck,
|
|
* Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper,
|
|
* Jeremias Maerki, Stephen Colebourne
|
|
*/
|
|
/**
|
|
* Normalizes a path, removing double and single dot path steps.
|
|
*
|
|
*
|
|
* This method normalizes a path to a standard format.
|
|
* The input may contain separators in either Unix or Windows format.
|
|
* The output will contain separators in the format specified.
|
|
*
|
|
*
|
|
* A trailing slash will be retained.
|
|
* A double slash will be merged to a single slash (but UNC names are handled).
|
|
* A single dot path segment will be removed.
|
|
* A double dot will cause that path segment and the one before to be removed.
|
|
* If the double dot has no parent path segment to work with, `null`
|
|
* is returned.
|
|
*
|
|
*
|
|
* The output will be the same on both Unix and Windows except
|
|
* for the separator character.
|
|
* <pre>
|
|
* /foo// --> /foo/
|
|
* /foo/./ --> /foo/
|
|
* /foo/../bar --> /bar
|
|
* /foo/../bar/ --> /bar/
|
|
* /foo/../bar/../baz --> /baz
|
|
* //foo//./bar --> /foo/bar
|
|
* /../ --> null
|
|
* ../foo --> null
|
|
* foo/bar/.. --> foo/
|
|
* foo/../../bar --> null
|
|
* foo/../bar --> bar
|
|
* //server/foo/../bar --> //server/bar
|
|
* //server/../bar --> null
|
|
* C:\foo\..\bar --> C:\bar
|
|
* C:\..\bar --> null
|
|
* ~/foo/../bar/ --> ~/bar/
|
|
* ~/../bar --> null
|
|
</pre> *
|
|
* The output will be the same on both Unix and Windows including
|
|
* the separator character.
|
|
*
|
|
* @param filename the filename to normalize, null returns null
|
|
* @param unixSeparator `true` if a unix separator should
|
|
* be used or `false` if a windows separator should be used.
|
|
* @return the normalized filename, or null if invalid
|
|
*/
|
|
fun normalize(filename: String, unixSeparator: Boolean): String? {
|
|
val separator = if (unixSeparator) UNIX_SEPARATOR else WINDOWS_SEPARATOR
|
|
return doNormalize(filename, separator, true)
|
|
}
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------
|
|
/*
|
|
* FilenameUtils.java (normalize + dependencies) - Apache 2.0 License
|
|
* http://commons.apache.org/proper/commons-io/
|
|
* Copyright 2013 ASF
|
|
* Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck,
|
|
* Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper,
|
|
* Jeremias Maerki, Stephen Colebourne
|
|
*/
|
|
/**
|
|
* Normalizes a path, removing double and single dot path steps,
|
|
* and removing any final directory separator.
|
|
*
|
|
*
|
|
* This method normalizes a path to a standard format.
|
|
* The input may contain separators in either Unix or Windows format.
|
|
* The output will contain separators in the format of the system.
|
|
*
|
|
*
|
|
* A trailing slash will be removed.
|
|
* A double slash will be merged to a single slash (but UNC names are handled).
|
|
* A single dot path segment will be removed.
|
|
* A double dot will cause that path segment and the one before to be removed.
|
|
* If the double dot has no parent path segment to work with, `null`
|
|
* is returned.
|
|
*
|
|
*
|
|
* The output will be the same on both Unix and Windows except
|
|
* for the separator character.
|
|
* <pre>
|
|
* /foo// --> /foo
|
|
* /foo/./ --> /foo
|
|
* /foo/../bar --> /bar
|
|
* /foo/../bar/ --> /bar
|
|
* /foo/../bar/../baz --> /baz
|
|
* //foo//./bar --> /foo/bar
|
|
* /../ --> null
|
|
* ../foo --> null
|
|
* foo/bar/.. --> foo
|
|
* foo/../../bar --> null
|
|
* foo/../bar --> bar
|
|
* //server/foo/../bar --> //server/bar
|
|
* //server/../bar --> null
|
|
* C:\foo\..\bar --> C:\bar
|
|
* C:\..\bar --> null
|
|
* ~/foo/../bar/ --> ~/bar
|
|
* ~/../bar --> null
|
|
</pre> *
|
|
* (Note the file separator returned will be correct for Windows/Unix)
|
|
*
|
|
* @param filename the filename to normalize, null returns null
|
|
* @return the normalized filename, or null if invalid
|
|
*/
|
|
fun normalizeNoEndSeparator(filename: String): String? {
|
|
return doNormalize(filename, SYSTEM_SEPARATOR, false)
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* FilenameUtils.java (normalize + dependencies) - Apache 2.0 License
|
|
* http://commons.apache.org/proper/commons-io/
|
|
* Copyright 2013 ASF
|
|
* Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck,
|
|
* Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper,
|
|
* Jeremias Maerki, Stephen Colebourne
|
|
*/
|
|
/**
|
|
* Normalizes a path, removing double and single dot path steps,
|
|
* and removing any final directory separator.
|
|
*
|
|
*
|
|
* This method normalizes a path to a standard format.
|
|
* The input may contain separators in either Unix or Windows format.
|
|
* The output will contain separators in the format specified.
|
|
*
|
|
*
|
|
* A trailing slash will be removed.
|
|
* A double slash will be merged to a single slash (but UNC names are handled).
|
|
* A single dot path segment will be removed.
|
|
* A double dot will cause that path segment and the one before to be removed.
|
|
* If the double dot has no parent path segment to work with, `null`
|
|
* is returned.
|
|
*
|
|
*
|
|
* The output will be the same on both Unix and Windows including
|
|
* the separator character.
|
|
* <pre>
|
|
* /foo// --> /foo
|
|
* /foo/./ --> /foo
|
|
* /foo/../bar --> /bar
|
|
* /foo/../bar/ --> /bar
|
|
* /foo/../bar/../baz --> /baz
|
|
* //foo//./bar --> /foo/bar
|
|
* /../ --> null
|
|
* ../foo --> null
|
|
* foo/bar/.. --> foo
|
|
* foo/../../bar --> null
|
|
* foo/../bar --> bar
|
|
* //server/foo/../bar --> //server/bar
|
|
* //server/../bar --> null
|
|
* C:\foo\..\bar --> C:\bar
|
|
* C:\..\bar --> null
|
|
* ~/foo/../bar/ --> ~/bar
|
|
* ~/../bar --> null
|
|
</pre> *
|
|
*
|
|
* @param filename the filename to normalize, null returns null
|
|
* @param unixSeparator `true` if a unix separator should
|
|
* be used or `false` if a windows separtor should be used.
|
|
* @return the normalized filename, or null if invalid
|
|
*/
|
|
fun normalizeNoEndSeparator(filename: String, unixSeparator: Boolean): String? {
|
|
val separator = if (unixSeparator) UNIX_SEPARATOR else WINDOWS_SEPARATOR
|
|
return doNormalize(filename, separator, false)
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* FilenameUtils.java (normalize + dependencies) - Apache 2.0 License
|
|
* http://commons.apache.org/proper/commons-io/
|
|
* Copyright 2013 ASF
|
|
* Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck,
|
|
* Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper,
|
|
* Jeremias Maerki, Stephen Colebourne
|
|
*/
|
|
/**
|
|
* Internal method to perform the normalization.
|
|
*
|
|
* @param filename the filename
|
|
* @param separator The separator character to use
|
|
* @param keepSeparator true to keep the final separator
|
|
* @return the normalized filename
|
|
*/
|
|
private fun doNormalize(filename: String, separator: Char, keepSeparator: Boolean): String? {
|
|
var size = filename.length
|
|
if (size == 0) {
|
|
return filename
|
|
}
|
|
|
|
val prefix = getPrefixLength(filename)
|
|
if (prefix < 0) {
|
|
return null
|
|
}
|
|
|
|
val array = CharArray(size + 2) // +1 for possible extra slash, +2 for arraycopy
|
|
filename.toCharArray(array, 0, 0, filename.length)
|
|
|
|
// fix separators throughout
|
|
val otherSeparator = if (separator == SYSTEM_SEPARATOR) OTHER_SEPARATOR else SYSTEM_SEPARATOR
|
|
for (i in array.indices) {
|
|
if (array[i] == otherSeparator) {
|
|
array[i] = separator
|
|
}
|
|
}
|
|
|
|
// add extra separator on the end to simplify code below
|
|
var lastIsDirectory = true
|
|
if (array[size - 1] != separator) {
|
|
array[size++] = separator
|
|
lastIsDirectory = false
|
|
}
|
|
|
|
// adjoining slashes
|
|
run {
|
|
var i = prefix + 1
|
|
while (i < size) {
|
|
if (array[i] == separator && array[i - 1] == separator) {
|
|
System.arraycopy(array, i, array, i - 1, size - i)
|
|
size--
|
|
i--
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
|
|
// dot slash
|
|
var i = prefix + 1
|
|
while (i < size) {
|
|
if (array[i] == separator && array[i - 1] == '.' && (i == prefix + 1 || array[i - 2] == separator)) {
|
|
if (i == size - 1) {
|
|
lastIsDirectory = true
|
|
}
|
|
System.arraycopy(array, i + 1, array, i - 1, size - i)
|
|
size -= 2
|
|
i--
|
|
}
|
|
i++
|
|
}
|
|
|
|
i = prefix + 2
|
|
outer@ while (i < size) {
|
|
if (array[i] == separator && array[i - 1] == '.' && array[i - 2] == '.' && (i == prefix + 2 || array[i - 3] == separator)) {
|
|
if (i == prefix + 2) {
|
|
return null
|
|
}
|
|
if (i == size - 1) {
|
|
lastIsDirectory = true
|
|
}
|
|
var j: Int
|
|
j = i - 4
|
|
while (j >= prefix) {
|
|
if (array[j] == separator) {
|
|
// remove b/../ from a/b/../c
|
|
System.arraycopy(array, i + 1, array, j + 1, size - i)
|
|
size -= i - j
|
|
i = j + 1
|
|
i++
|
|
continue@outer
|
|
}
|
|
j--
|
|
}
|
|
// remove a/../ from a/../c
|
|
System.arraycopy(array, i + 1, array, prefix, size - i)
|
|
size -= i + 1 - prefix
|
|
i = prefix + 1
|
|
}
|
|
i++
|
|
}
|
|
|
|
if (size <= 0) { // should never be less than 0
|
|
return ""
|
|
}
|
|
|
|
if (size <= prefix) { // should never be less than prefix
|
|
return String(array, 0, size)
|
|
}
|
|
|
|
return if (lastIsDirectory && keepSeparator) {
|
|
String(array, 0, size) // keep trailing separator
|
|
} else String(array, 0, size - 1)
|
|
// lose trailing separator
|
|
}
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------
|
|
/*
|
|
* FilenameUtils.java (normalize + dependencies) - Apache 2.0 License
|
|
* http://commons.apache.org/proper/commons-io/
|
|
* Copyright 2013 ASF
|
|
* Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck,
|
|
* Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper,
|
|
* Jeremias Maerki, Stephen Colebourne
|
|
*/
|
|
/**
|
|
* Returns the length of the filename prefix, such as `C:/` or `~/`.
|
|
*
|
|
*
|
|
* This method will handle a file in either Unix or Windows format.
|
|
*
|
|
*
|
|
* The prefix length includes the first slash in the full filename
|
|
* if applicable. Thus, it is possible that the length returned is greater
|
|
* than the length of the input string.
|
|
````
|
|
Windows:
|
|
a\b\c.txt --> "" --> relative
|
|
\a\b\c.txt --> "\" --> current drive absolute
|
|
C:a\b\c.txt --> "C:" --> drive relative
|
|
C:\a\b\c.txt --> "C:\" --> absolute
|
|
\\server\a\b\c.txt --> "\\server\" --> UNC
|
|
|
|
Unix:
|
|
a/b/c.txt --> "" --> relative
|
|
/a/b/c.txt --> "/" --> absolute
|
|
~/a/b/c.txt --> "~/" --> current user
|
|
~ --> "~/" --> current user (slash added)
|
|
~user/a/b/c.txt --> "~user/" --> named user
|
|
~user --> "~user/" --> named user (slash added)
|
|
````
|
|
*
|
|
*
|
|
*
|
|
* The output will be the same irrespective of the machine that the code is running on.
|
|
* ie. both Unix and Windows prefixes are matched regardless.
|
|
*
|
|
* @param filename the filename to find the prefix in, null returns -1
|
|
* @return the length of the prefix, -1 if invalid or null
|
|
*/
|
|
fun getPrefixLength(filename: String): Int {
|
|
val len = filename.length
|
|
if (len == 0) {
|
|
return 0
|
|
}
|
|
|
|
var ch0 = filename[0]
|
|
if (ch0 == ':') {
|
|
return -1
|
|
}
|
|
|
|
return if (len == 1) {
|
|
if (ch0 == '~') {
|
|
return 2 // return a length greater than the input
|
|
}
|
|
if (isSeparator(ch0)) 1 else 0
|
|
} else {
|
|
if (ch0 == '~') {
|
|
var posUnix = filename.indexOf(UNIX_SEPARATOR, 1)
|
|
var posWin = filename.indexOf(WINDOWS_SEPARATOR, 1)
|
|
if (posUnix == -1 && posWin == -1) {
|
|
return len + 1 // return a length greater than the input
|
|
}
|
|
posUnix = if (posUnix == -1) posWin else posUnix
|
|
posWin = if (posWin == -1) posUnix else posWin
|
|
return Math.min(posUnix, posWin) + 1
|
|
}
|
|
val ch1 = filename[1]
|
|
if (ch1 == ':') {
|
|
ch0 = ch0.uppercaseChar()
|
|
if (ch0 >= 'A' && ch0 <= 'Z') {
|
|
return if (len == 2 || isSeparator(filename[2]) == false) {
|
|
2
|
|
} else 3
|
|
}
|
|
-1
|
|
} else if (isSeparator(ch0) && isSeparator(ch1)) {
|
|
var posUnix = filename.indexOf(UNIX_SEPARATOR, 2)
|
|
var posWin = filename.indexOf(WINDOWS_SEPARATOR, 2)
|
|
if (posUnix == -1 && posWin == -1 || posUnix == 2 || posWin == 2) {
|
|
return -1
|
|
}
|
|
posUnix = if (posUnix == -1) posWin else posUnix
|
|
posWin = if (posWin == -1) posUnix else posWin
|
|
Math.min(posUnix, posWin) + 1
|
|
} else {
|
|
if (isSeparator(ch0)) 1 else 0
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------
|
|
/*
|
|
* FilenameUtils.java (normalize + dependencies) - Apache 2.0 License
|
|
* http://commons.apache.org/proper/commons-io/
|
|
* Copyright 2013 ASF
|
|
* Authors: Kevin A. Burton, Scott Sanders, Daniel Rall, Christoph.Reck,
|
|
* Peter Donald, Jeff Turner, Matthew Hawthorne, Martin Cooper,
|
|
* Jeremias Maerki, Stephen Colebourne
|
|
*/
|
|
/**
|
|
* Checks if the character is a separator.
|
|
*
|
|
* @param ch the character to check
|
|
* @return true if it is a separator character
|
|
*/
|
|
private fun isSeparator(ch: Char): Boolean {
|
|
return ch == UNIX_SEPARATOR || ch == WINDOWS_SEPARATOR
|
|
}
|
|
|
|
/**
|
|
* Gets the extension of a file (text after the last '.')
|
|
*
|
|
* @return "" if there is no extension
|
|
*/
|
|
fun getExtension(fileName: String): String {
|
|
val dot = fileName.lastIndexOf('.')
|
|
return if (dot > -1) {
|
|
fileName.substring(dot + 1)
|
|
} else {
|
|
""
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the name of a file that is before the extension (text before the last '.')
|
|
*
|
|
* @return non-null
|
|
*/
|
|
fun getNameWithoutExtension(fileName: String): String {
|
|
val dot = fileName.lastIndexOf('.')
|
|
return if (dot > -1) {
|
|
fileName.substring(0, dot)
|
|
} else {
|
|
fileName
|
|
}
|
|
}
|
|
}
|