secant-android-wallet/spackle-lib/src/jvmMain/kotlin/co/electriccoin/zcash/spackle/io/FileExt.kt

88 lines
2.5 KiB
Kotlin

@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.spackle.io
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.util.UUID
suspend fun File.existsSuspend() = withContext(Dispatchers.IO) {
exists()
}
suspend fun File.mkdirsSuspend() = withContext(Dispatchers.IO) {
mkdirs()
}
suspend fun File.isDirectorySuspend() = withContext(Dispatchers.IO) {
isDirectory
}
suspend fun File.isFileSuspend() = withContext(Dispatchers.IO) {
isFile
}
suspend fun File.canWriteSuspend() = withContext(Dispatchers.IO) {
canWrite()
}
suspend fun File.deleteSuspend() = withContext(Dispatchers.IO) {
delete()
}
suspend fun File.renameToSuspend(destination: File) = withContext(Dispatchers.IO) {
renameTo(destination)
}
suspend fun File.listFilesSuspend() = withContext(Dispatchers.IO) {
listFiles()
}
/**
* Given an ultimate output file destination, this generates a temporary file that [action] can write to. After action
* is complete, the temp file is renamed to the expected output destination. Depending on the underlying filesystem,
* this should effectively ensure that the file is perceived as being written atomically.
*
* @receiver Ultimate file that we desire to write to. Must be a file and not a directory.
* @param action Action to perform on the file, specifically this should be writing data. This action should not
* delete, rename, or do other operations in the filesystem.
*/
suspend fun File.writeAtomically(action: (suspend (File) -> Unit)) {
val tempFile = withContext(Dispatchers.IO) {
File(parentFile, name.newTempFileName()).also {
it.deleteOnExit()
}
}
var isWriteSuccessful = false
try {
action(tempFile)
isWriteSuccessful = true
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
tempFile.deleteSuspend()
throw e
} finally {
if (isWriteSuccessful) {
tempFile.moveTo(destination = this)
}
}
}
private suspend fun File.moveTo(destination: File) {
val isRenameSuccessful = renameToSuspend(destination)
if (!isRenameSuccessful) {
if (existsSuspend()) {
throw IOException("Couldn't move file $path to ${destination.path}")
}
// Otherwise no data was written, so there's no file to rename.
}
}
// Note that adding uuid and .tmp could theoretically go past file name length limits on some filesystems
private fun String.newTempFileName() = "$this-${UUID.randomUUID()}.tmp"