
dorkbox.util.FileUtil.kt Maven / Gradle / Ivy
/*
* Copyright 2023 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 org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.*
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 = Sys.version
private val log: Logger = LoggerFactory.getLogger(FileUtil::class.java)
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()
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) {
// 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) {
// 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 {
val lines: MutableList = 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 {
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 {
val lines: MutableList = 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 {
val list = ArrayList()
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 {
val list = ArrayList()
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, 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 = `in`.normalize().absolutePath
val normalizedOut = out.normalize().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 = src_.normalize()
val dest = dest_.normalize()
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 = file2.normalize().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 = location.normalize().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 File.createTempFile(fileName, null).normalize().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 file.normalize().absolutePath
}
/**
* @return true if the inputStream is a zip/jar stream. DOES NOT CLOSE THE STREAM
*/
fun isZipStream(`in`: InputStream): Boolean {
@Suppress("NAME_SHADOWING")
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 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 {
return parseDir(File(rootDirectory))
}
/**
* Parses the specified root directory for **ALL** files that are in it. All 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 {
return parseDir(rootDirectory)
}
/**
* Parses the specified root directory for files that end in the extension to match. All 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 {
val jarList: MutableList = LinkedList()
val directories = LinkedList()
@Suppress("NAME_SHADOWING")
val rootDirectory = rootDirectory.normalize()
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? = 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 tempOutput = FileOutputStream(tempFile)
tempOutput.use {
inputStream.copyTo(it)
}
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)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy