Refine EhTagDatabase & Cleanup java io usage

* Further kill some java io usage

* Further optimize TagDatabase update logic

* Further optimize TagDatabase

* Move some part from okio to kxio

* Address Code Review

* Fix to not reinit with current if inited

* Convert readLegacySpiderInfo to use kxio

* Refine

* Refine

* Refine

---------

Co-authored-by: Dude so hot <djohn@fbi.gov>
This commit is contained in:
renovate[bot]andDude so hot authored and GitHub committed 2024-12-08 14:30:56 +00:00
1 parent 7bd36dd274
commit 8c766115db
20 files changed
+169 -238

No files matched your search

@@ -70,6 +70,7 @@ import com.hippo.ehviewer.util.isAtLeastO
import com.hippo.ehviewer.util.isAtLeastP
import com.hippo.ehviewer.util.isAtLeastS
import com.hippo.ehviewer.util.isAtLeastSExtension7
import com.hippo.files.deleteContent
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
@@ -112,7 +113,7 @@ class EhApplication :
System.loadLibrary("ehviewer")
lifecycleScope.launchIO {
launchUI { FavouriteStatusRouter.collect { (gid, slot) -> detailCache[gid]?.favoriteSlot = slot } }
launch { EhTagDatabase }
EhTagDatabase.launchUpdate()
launch { EhDB }
launchIO { dataStateFlow.value }
launchIO { OSUtils.totalMemory }
@@ -150,14 +151,8 @@ class EhApplication :
}
private fun clearTempDir() {
var dir = AppConfig.tempDir
if (null != dir) {
FileUtils.deleteContent(dir)
}
dir = AppConfig.externalTempDir
if (null != dir) {
FileUtils.deleteContent(dir)
}
AppConfig.tempDir.deleteContent()
AppConfig.externalTempDir?.deleteContent()
}
override fun newImageLoader(context: Context) = context.imageLoader {
@@ -65,11 +65,5 @@ fun updateWhenGallerySiteChanges() {
}
fun updateWhenTagTranslationChanges() {
collectScope.launchIO {
runCatching {
EhTagDatabase.update()
}.onFailure {
logcat(it)
}
}
EhTagDatabase.launchUpdate()
}
@@ -32,13 +32,13 @@ import io.ktor.http.HttpMethod
import io.ktor.http.ParametersBuilder
import io.ktor.http.content.TextContent
import kotlinx.coroutines.cancel
import kotlinx.io.Source
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArrayBuilder
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.okio.decodeFromBufferedSource
import okio.BufferedSource
import kotlinx.serialization.json.io.decodeFromSource
inline fun JsonObjectBuilder.array(name: String, builder: JsonArrayBuilder.() -> Unit) = put(name, buildJsonArray(builder))
@@ -47,7 +47,7 @@ suspend inline fun <reified T> HttpStatement.executeAndParseAs() = executeSafely
it.bodyAsUtf8Text().parseAs<T>()
}
inline fun <reified T> BufferedSource.parseAs(): T = json.decodeFromBufferedSource(this)
inline fun <reified T> Source.parseAs(): T = json.decodeFromSource(this)
inline fun <reified T> String.parseAs(): T = json.decodeFromString(this)
val json = Json {
@@ -24,29 +24,30 @@ import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.data.TagNamespace
import com.hippo.ehviewer.ui.screen.implicit
import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.copyTo
import com.hippo.ehviewer.util.ensureSuccess
import com.hippo.ehviewer.util.sha1
import com.hippo.ehviewer.util.utf8
import com.hippo.files.delete
import com.hippo.files.moveTo
import com.hippo.files.read
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.client.HttpClient
import io.ktor.client.request.prepareGet
import io.ktor.client.statement.bodyAsChannel
import java.io.File
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.io.Source
import moe.tarsin.coroutines.runSuspendCatching
import okio.BufferedSource
import okio.Path.Companion.toOkioPath
import okio.buffer
import okio.source
import okio.Path
import splitties.init.appCtx
object EhTagDatabase : CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job()
private const val NAMESPACE_PREFIX = "n"
private val PREFIXES = arrayOf(
"-",
@@ -56,8 +57,6 @@ object EhTagDatabase : CoroutineScope {
)
private lateinit var tagGroups: Map<String, Map<String, String>>
private val updateLock = Mutex()
override val coroutineContext = Dispatchers.IO + Job()
var initialized by mutableStateOf(false)
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? = tagGroups[prefix]?.get(tag)?.trim()?.ifEmpty { null }
@@ -101,18 +100,12 @@ object EhTagDatabase : CoroutineScope {
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean = removeSpace().contains(other.removeSpace(), ignoreCase)
private fun getMetadata(context: Context): Array<String>? = context.resources.getStringArray(R.array.tag_translation_metadata)
private fun metadata(context: Context): Array<String>? = context.resources.getStringArray(R.array.tag_translation_metadata)
.takeIf { it.size == 4 }
fun isTranslatable(context: Context): Boolean = context.resources.getBoolean(R.bool.tag_translatable)
private fun getFileContent(file: File): String? = runCatching {
file.source().buffer().use { it.readUtf8() }
}.getOrNull()
private fun checkData(sha1: String?, data: File): Boolean = sha1 != null && sha1 == data.toOkioPath().sha1()
private suspend fun save(client: HttpClient, url: String, file: File) {
private suspend fun fetch(client: HttpClient, url: String, file: Path) {
runCatching {
client.prepareGet(url).executeSafely {
it.status.ensureSuccess()
@@ -123,89 +116,70 @@ object EhTagDatabase : CoroutineScope {
}.getOrThrow()
}
suspend fun update() {
fun launchUpdate() = launch {
updateLock.withLock {
updateInternal()
}
}
private fun issueUpdateInMemoryData(file: File? = null) {
val dataFile = file ?: getMetadata(appCtx)?.let { metadata ->
val dataName = metadata[2]
val dir = AppConfig.getFilesDir("tag-translations")
File(dir, dataName).takeIf { it.exists() }
} ?: return
tagGroups = dataFile.source().buffer().use(BufferedSource::parseAs)
initialized = true
}
init {
launch {
updateLock.withLock {
runSuspendCatching {
issueUpdateInMemoryData()
}.onFailure {
logcat(it)
}
updateInternal()
}
}
}
private suspend fun updateInternal() {
getMetadata(appCtx)?.let { metadata ->
val sha1Name = metadata[0]
val sha1Url = metadata[1]
val dataName = metadata[2]
val dataUrl = metadata[3]
val dir = AppConfig.getFilesDir("tag-translations")
checkNotNull(dir)
val sha1File = File(dir, sha1Name)
val dataFile = File(dir, dataName)
runSuspendCatching {
// Check current sha1 and current data
val sha1 = getFileContent(sha1File)
if (!checkData(sha1, dataFile)) {
FileUtils.delete(sha1File)
FileUtils.delete(dataFile)
}
// Save new sha1
val tempSha1File = File(dir, "$sha1Name.tmp")
save(ktorClient, sha1Url, tempSha1File)
val tempSha1 = getFileContent(tempSha1File)
// Check new sha1 and current sha1
if (tempSha1 == sha1) {
// The data is the same
FileUtils.delete(tempSha1File)
return
}
// Save new data
val tempDataFile = File(dir, "$dataName.tmp")
save(ktorClient, dataUrl, tempDataFile)
// Check new sha1 and new data
if (!checkData(tempSha1, tempDataFile)) {
FileUtils.delete(tempSha1File)
FileUtils.delete(tempDataFile)
return
}
// Replace current sha1 and current data with new sha1 and new data
FileUtils.delete(sha1File)
FileUtils.delete(dataFile)
tempSha1File.renameTo(sha1File)
tempDataFile.renameTo(dataFile)
// Read new EhTagDatabase
issueUpdateInMemoryData(dataFile)
atomicallyUpdate()
}.onFailure {
logcat(it)
}
}
}
private suspend fun atomicallyUpdate() {
val (sha1Name, sha1Url, dataName, dataUrl) = metadata(appCtx) ?: return
val workdir = AppConfig.tagTranslationsDir
val sha1File = workdir / sha1Name
val dataFile = workdir / dataName
fun updateInMemoryData() {
tagGroups = dataFile.read(Source::parseAs)
initialized = true
}
// Check current sha1 and current data
val currentSha1 = runSuspendCatching {
dataFile.sha1().also {
check(sha1File.utf8() == it)
if (!initialized) updateInMemoryData()
}
}.onFailure {
sha1File.delete()
dataFile.delete()
}.getOrNull()
// Save new sha1
val tempSha1File = workdir / "$sha1Name.tmp"
fetch(ktorClient, sha1Url, tempSha1File)
val tempSha1 = tempSha1File.utf8()
// Check new sha1 and current sha1
if (tempSha1 == currentSha1) {
// The data is the same
tempSha1File.delete()
return
}
// Save new data
val tempDataFile = workdir / "$dataName.tmp"
fetch(ktorClient, dataUrl, tempDataFile)
// Check new sha1 and new data
runSuspendCatching {
check(tempDataFile.sha1() == tempSha1)
}.onFailure {
tempSha1File.delete()
tempDataFile.delete()
throw it
}.onSuccess {
// Replace current sha1 and current data with new sha1 and new data
sha1File.delete()
dataFile.delete()
tempSha1File moveTo sha1File
tempDataFile moveTo dataFile
// Read new EhTagDatabase
updateInMemoryData()
}
}
}
@@ -830,4 +830,4 @@ var downloadLocation: Path
val DownloadInfo.downloadDir get() = dirname?.let { downloadLocation / it }
val DownloadInfo.archiveFile get() = downloadDir?.run { find("$gid.cbz") ?: find("$gid.zip") }
val GalleryInfo.tempDownloadDir get() = AppConfig.getTempDir("$gid")?.toOkioPath()
val GalleryInfo.tempDownloadDir get() = AppConfig.externalTempPersistDir?.let { it / "$gid" }
@@ -1,17 +1,15 @@
package com.hippo.ehviewer.legacy
import com.hippo.ehviewer.spider.SpiderInfo
import java.io.InputStream
import okio.buffer
import okio.source
import com.hippo.files.read
import kotlinx.io.readLineStrict
import okio.Path
fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo {
val source = inputStream.source().buffer()
fun read(): String = source.readUtf8LineStrict()
fun Path.readLegacySpiderInfo() = read {
fun read() = readLineStrict()
fun readInt() = read().toInt()
fun readLong() = read().toLong()
fun readInt(): Int = read().toInt()
fun readLong(): Long = read().toLong()
repeat(2) { read() } // We assert that only info v2
val gid = readLong()
val token = read()
@@ -20,19 +18,19 @@ fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo {
val previewPerPage = readInt()
val pages = read().toInt()
val pTokenMap = hashMapOf<Int, String>()
val info = SpiderInfo(gid, token, pages, pTokenMap, previewPages, previewPerPage)
runCatching {
while (true) {
val line = read()
val pos = line.indexOf(" ")
if (pos > 0) {
val index = line.substring(0, pos).toInt()
val pToken = line.substring(pos + 1)
if (pToken.isNotEmpty()) {
pTokenMap[index] = pToken
SpiderInfo(gid, token, pages, pTokenMap, previewPages, previewPerPage).apply {
runCatching {
while (true) {
val line = read()
val pos = line.indexOf(" ")
if (pos > 0) {
val index = line.substring(0, pos).toInt()
val pToken = line.substring(pos + 1)
if (pToken.isNotEmpty()) {
pTokenMap[index] = pToken
}
}
}
}
}
return info
}
@@ -16,8 +16,8 @@ import com.hippo.ehviewer.client.data.TagNamespace.Male
import com.hippo.ehviewer.client.data.TagNamespace.Mixed
import com.hippo.ehviewer.client.data.TagNamespace.Other
import com.hippo.ehviewer.client.data.TagNamespace.Parody
import com.hippo.files.openInputStream
import com.hippo.files.openOutputStream
import com.hippo.files.inputStream
import com.hippo.files.outputStream
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -107,7 +107,7 @@ fun ComicInfo.toSimpleTags() = listOfNotNull(
).flatten().ifEmpty { null }
fun ComicInfo.write(file: Path) {
file.openOutputStream().bufferedWriter().use {
file.outputStream().bufferedWriter().use {
xmlStreaming.newWriter(it).use { writer ->
xml.encodeToWriter(writer, ComicInfo.serializer(), this)
}
@@ -115,7 +115,7 @@ fun ComicInfo.write(file: Path) {
}
fun readComicInfo(file: Path): ComicInfo? = runCatching {
file.openInputStream().bufferedReader().use {
file.inputStream().bufferedReader().use {
xmlStreaming.newReader(it).use { reader ->
xml.decodeFromReader(ComicInfo.serializer(), reader)
}
@@ -49,7 +49,7 @@ import com.hippo.files.list
import com.hippo.files.mkdirs
import com.hippo.files.moveTo
import com.hippo.files.openFileDescriptor
import com.hippo.files.openOutputStream
import com.hippo.files.outputStream
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.client.plugins.onDownload
import io.ktor.client.plugins.timeout
@@ -198,7 +198,7 @@ class SpiderDen(val info: GalleryInfo) {
val file = resolve(tempFile.name.removeSuffix(TEMP_SUFFIX))
file.delete()
lock.write { fileCache.remove(file.name) }
tempFile.moveTo(file)
tempFile moveTo file
lock.write {
fileCache.remove(tempFile.name)
fileCache[file.name] = file
@@ -221,7 +221,7 @@ class SpiderDen(val info: GalleryInfo) {
val url = response.request.url.toString()
val extension = MimeTypeMap.getFileExtensionFromUrl(url).ifEmpty { "jpg" }
return saveResponseMeta(index, extension) { outFile ->
outFile.openOutputStream().use {
outFile.outputStream().use {
response.bodyAsChannel().copyTo(it.channel)
}
FileHashRegex.find(url)?.let {
@@ -4,9 +4,10 @@ import com.hippo.ehviewer.coil.edit
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.ktbuilder.diskCache
import com.hippo.ehviewer.legacy.readLegacySpiderInfo
import com.hippo.files.openInputStream
import com.hippo.files.openOutputStream
import com.hippo.files.outputStream
import com.hippo.files.read
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.io.readByteArray
import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.decodeFromByteArray
@@ -36,7 +37,7 @@ private val cbor = Cbor {
}
fun SpiderInfo.write(file: Path) {
file.openOutputStream().use {
file.outputStream().use {
it.write(cbor.encodeToByteArray(this))
}
}
@@ -60,18 +61,14 @@ private val spiderInfoCache by lazy {
fun readFromCache(gid: Long): SpiderInfo? = spiderInfoCache.read(gid.toString()) {
runCatching {
cbor.decodeFromByteArray<SpiderInfo>(data.toFile().readBytes())
data.read { cbor.decodeFromByteArray<SpiderInfo>(readByteArray()) }
}.onFailure {
logcat(it)
}.getOrNull()
}
fun readCompatFromPath(file: Path): SpiderInfo? = runCatching {
file.openInputStream().use {
cbor.decodeFromByteArray<SpiderInfo>(it.readBytes())
}
file.read { cbor.decodeFromByteArray<SpiderInfo>(readByteArray()) }
}.getOrNull() ?: runCatching {
file.openInputStream().use { readLegacySpiderInfo(it) }
file.readLegacySpiderInfo()
}.getOrNull()
const val TOKEN_FAILED = "failed"
@@ -68,7 +68,7 @@ import com.hippo.ehviewer.util.toEpochMillis
import com.hippo.ehviewer.util.toLocalDateTime
import com.hippo.files.delete
import com.hippo.files.isDirectory
import com.hippo.files.openOutputStream
import com.hippo.files.outputStream
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
@@ -89,7 +89,7 @@ private fun removeNoMediaFile(downloadDir: Path) {
}
private fun ensureNoMediaFile(downloadDir: Path) {
(downloadDir / ".nomedia").openOutputStream().close()
(downloadDir / ".nomedia").outputStream().close()
}
private val lck = Mutex()
@@ -37,8 +37,8 @@ context(PageLoader)
private fun Context.provideImage(index: Int): Uri? {
val dir = AppConfig.externalTempDir ?: return null
val name = getImageFilename(index) ?: return null
val file = File(dir, name).takeIf { save(index, it.toOkioPath()) } ?: return null
return FileProvider.getUriForFile(this, "$APPLICATION_ID.fileprovider", file)
val file = (dir / name).takeIf { save(index, it) } ?: return null
return FileProvider.getUriForFile(this, "$APPLICATION_ID.fileprovider", file.toFile())
}
context(SnackbarHostState, Context, PageLoader)
@@ -44,11 +44,11 @@ import com.hippo.ehviewer.util.AppConfig
import com.hippo.ehviewer.util.ReadableTime
import com.hippo.ehviewer.util.displayString
import com.hippo.ehviewer.util.installPackage
import com.hippo.files.delete
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import eu.kanade.tachiyomi.util.lang.withUIContext
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
@@ -157,7 +157,7 @@ suspend fun DialogState.showNewVersion(context: Context, release: Release) {
EhDB.exportDB(context, (downloadLocation / "$time.db"))
}
// TODO: Download in the background and show progress in notification
val file = File(AppConfig.tempDir, "update.apk").apply { delete() }
AppUpdater.downloadUpdate(release.downloadLink, file)
withUIContext { context.installPackage(file) }
val path = AppConfig.tempDir / "update.apk"
AppUpdater.downloadUpdate(release.downloadLink, path.apply { delete() })
withUIContext { context.installPackage(path.toFile()) }
}
@@ -7,18 +7,19 @@ import com.hippo.ehviewer.client.executeAndParseAs
import com.hippo.ehviewer.client.executeSafely
import com.hippo.ehviewer.util.copyTo
import com.hippo.ehviewer.util.ensureSuccess
import com.hippo.files.outputStream
import io.ktor.client.plugins.timeout
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.header
import io.ktor.client.request.prepareGet
import io.ktor.client.statement.bodyAsChannel
import io.ktor.utils.io.jvm.javaio.toInputStream
import java.io.File
import java.util.zip.ZipInputStream
import kotlin.time.Duration.Companion.days
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import moe.tarsin.coroutines.runSuspendCatching
import okio.Path
import tachiyomi.data.release.GithubArtifacts
import tachiyomi.data.release.GithubCommitComparison
import tachiyomi.data.release.GithubRelease
@@ -68,7 +69,7 @@ object AppUpdater {
return null
}
suspend fun downloadUpdate(url: String, file: File) = ghStatement(url) {
suspend fun downloadUpdate(url: String, path: Path) = ghStatement(url) {
timeout {
requestTimeoutMillis = 60_000
}
@@ -78,13 +79,13 @@ object AppUpdater {
response.bodyAsChannel().toInputStream().use { stream ->
ZipInputStream(stream).use { zip ->
zip.nextEntry
file.outputStream().use {
path.outputStream().use {
zip.copyTo(it)
}
}
}
} else {
response.bodyAsChannel().copyTo(file)
response.bodyAsChannel().copyTo(path)
}
}
}
@@ -19,17 +19,25 @@ import android.os.Build
import android.os.Environment
import com.hippo.ehviewer.BuildConfig
import com.hippo.ehviewer.client.parser.ParserUtils
import com.hippo.files.exists
import com.hippo.files.isDirectory
import com.hippo.files.mkdirs
import java.io.File
import okio.Path
import okio.Path.Companion.toOkioPath
import splitties.init.appCtx
fun File.ensureDirectory() = if (exists()) isDirectory else mkdirs()
fun Path.ensureDirectory() = if (exists()) isDirectory else mkdirs().let { true }
object AppConfig {
const val APP_DIRNAME = "EhViewer"
private const val DOWNLOAD = "download"
private const val TEMP = "temp"
private const val PARSE_ERROR = "parse_error"
private const val CRASH = "crash"
private const val TAG_TRANSLATIONS = "tag-translations"
private val abi = Build.SUPPORTED_ABIS[0].takeIf {
it in setOf("arm64-v8a", "x86_64", "armeabi-v7a")
@@ -55,20 +63,18 @@ object AppConfig {
val defaultDownloadDir: File?
get() = getDirInExternalAppDir(DOWNLOAD, false)
fun getTempDir(filename: String): File? = getDirInExternalAppDir(TEMP)?.run { File(this, filename) }
val externalTempDir: File?
get() = appCtx.externalCacheDir?.run { File(this, TEMP).takeIf { it.ensureDirectory() } }
val externalTempPersistDir
get() = getDirInExternalAppDir(TEMP)?.toOkioPath()
val externalParseErrorDir: File?
get() = getDirInExternalAppDir(PARSE_ERROR)
val externalCrashDir: File?
get() = getDirInExternalAppDir(CRASH)
val tempDir: File?
get() = appCtx.cacheDir.run { File(this, TEMP).takeIf { it.ensureDirectory() } }
val tagTranslationsDir
get() = (appCtx.filesDir.toOkioPath() / TAG_TRANSLATIONS).apply { check(ensureDirectory()) }
fun createTempFile(): File? = FileUtils.createTempFile(tempDir, null)
fun getFilesDir(name: String) = File(appCtx.filesDir, name).takeIf { it.ensureDirectory() }
// Following locations will be clear on app startup
val tempDir
get() = (appCtx.cacheDir.toOkioPath() / TEMP).apply { check(ensureDirectory()) }
val externalTempDir
get() = appCtx.externalCacheDir?.toOkioPath()?.let { it / TEMP }?.apply { check(ensureDirectory()) }
}
@@ -4,9 +4,9 @@ import android.os.Build
import android.system.Int64Ref
import android.system.Os
import androidx.annotation.RequiresApi
import com.hippo.files.inputStream
import com.hippo.files.openFileDescriptor
import com.hippo.files.openInputStream
import com.hippo.files.openOutputStream
import com.hippo.files.outputStream
import eu.kanade.tachiyomi.util.system.logcat
import java.io.FileDescriptor
import okio.Path
@@ -27,8 +27,8 @@ infix fun Path.sendTo(file: Path): Long {
logcat("sendfile", e)
}
}
return openInputStream().use { src ->
file.openOutputStream().use { dst ->
return inputStream().use { src ->
file.outputStream().use { dst ->
src.channel.transferTo(0, Long.MAX_VALUE, dst.channel)
}
}
@@ -15,13 +15,14 @@
*/
package com.hippo.ehviewer.util
import com.hippo.files.outputStream
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.copyTo
import java.io.File
import java.io.RandomAccessFile
import java.util.Locale
import kotlin.math.ln
import kotlin.math.pow
import okio.Path
object FileUtils {
// Even though vfat allows 255 UCS-2 chars, we might eventually write to
@@ -51,26 +52,6 @@ object FileUtils {
)
}
/**
* Try to delete file, dir and it's children
*
* @param file the file to delete
* The dir to deleted
*/
fun delete(file: File?): Boolean {
file ?: return false
return deleteContent(file) and file.delete()
}
fun deleteContent(file: File?): Boolean {
file ?: return false
var success = true
file.listFiles()?.forEach {
success = success and delete(it)
}
return success
}
fun cleanupDirectory(dir: File?, maxFiles: Int = 10) {
dir?.listFiles()?.let { files ->
files.sortByDescending { it.lastModified() }
@@ -142,36 +123,10 @@ object FileUtils {
* @return null for start with . dot
*/
fun getNameFromFilename(filename: String?) = filename?.substringBeforeLast('.')?.ifEmpty { null }
/**
* Create a temp file, you need to delete it by you self.
*
* @param parent The temp file's parent
* @param extension The extension of temp file
* @return The temp file or null
*/
fun createTempFile(parent: File?, extension: String?): File? {
parent ?: return null
val now = System.currentTimeMillis()
for (i in 0..99) {
var filename = (now + i).toString()
extension?.let {
filename = "$filename.$it"
}
val tempFile = File(parent, filename)
if (!tempFile.exists()) {
return tempFile
}
}
// Unbelievable
return null
}
}
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun ByteReadChannel.copyTo(file: File) {
RandomAccessFile(file, "rw").use {
suspend fun ByteReadChannel.copyTo(file: Path) {
file.outputStream().use {
copyTo(it.channel)
}
}
@@ -6,8 +6,11 @@ import android.os.Environment
import android.provider.DocumentsContract
import androidx.core.provider.DocumentsContractCompat
import arrow.core.Either
import com.hippo.ehviewer.jni.sha1 as nativeSha1
import com.hippo.files.openFileDescriptor
import com.hippo.files.read
import com.hippo.files.toUri
import kotlinx.io.readString
import okio.Path
import splitties.init.appCtx
@@ -55,4 +58,5 @@ val Path.displayName: String
return name
}
fun Path.sha1() = openFileDescriptor("r").use { com.hippo.ehviewer.jni.sha1(it.fd) }
fun Path.sha1() = openFileDescriptor("r").use { nativeSha1(it.fd) }
fun Path.utf8() = read { readString() }
@@ -16,7 +16,8 @@ import okio.FileSystem
import okio.IOException
import okio.Path
import okio.Sink
import okio.Source
import okio.sink
import okio.source
import splitties.init.appCtx
class AndroidFileSystem(context: Context) : FileSystem() {
@@ -176,12 +177,11 @@ class AndroidFileSystem(context: Context) : FileSystem() {
}
override fun sink(file: Path, mustCreate: Boolean): Sink {
TODO("Not yet implemented")
if (mustCreate && exists(file)) throw IOException("$file already exists.")
return file.outputStream().sink()
}
override fun source(file: Path): Source {
TODO("Not yet implemented")
}
override fun source(file: Path) = file.inputStream().source()
fun openFileDescriptor(path: Path, mode: String): ParcelFileDescriptor {
if (path.isPhysicalFile()) {
+10 -3
View File
@@ -6,6 +6,9 @@ import android.os.ParcelFileDescriptor
import androidx.core.net.toUri
import java.io.FileInputStream
import java.io.FileOutputStream
import kotlinx.io.Source
import kotlinx.io.asSource
import kotlinx.io.buffered
import okio.Path
import okio.Path.Companion.toPath
@@ -21,17 +24,21 @@ fun Path.exists() = SystemFileSystem.exists(this)
fun Path.delete() = SystemFileSystem.deleteRecursively(this)
fun Path.deleteContent() = SystemFileSystem.listOrNull(this)?.forEach(Path::delete)
fun Path.list() = SystemFileSystem.listOrNull(this).orEmpty()
fun Path.mkdirs() = SystemFileSystem.createDirectories(this)
fun Path.moveTo(target: Path) = SystemFileSystem.atomicMove(this, target)
infix fun Path.moveTo(target: Path) = SystemFileSystem.atomicMove(this, target)
fun Path.openFileDescriptor(mode: String) = SystemFileSystem.openFileDescriptor(this, mode)
fun Path.openInputStream(): FileInputStream = ParcelFileDescriptor.AutoCloseInputStream(SystemFileSystem.openFileDescriptor(this, "r"))
fun Path.inputStream(): FileInputStream = ParcelFileDescriptor.AutoCloseInputStream(SystemFileSystem.openFileDescriptor(this, "r"))
fun Path.openOutputStream(): FileOutputStream = ParcelFileDescriptor.AutoCloseOutputStream(SystemFileSystem.openFileDescriptor(this, "wt"))
fun Path.outputStream(): FileOutputStream = ParcelFileDescriptor.AutoCloseOutputStream(SystemFileSystem.openFileDescriptor(this, "wt"))
inline fun <T> Path.read(f: Source.() -> T) = inputStream().asSource().buffered().use(f)
fun Path.toUri(): Uri {
val str = toString()
+2 -2
View File
@@ -106,7 +106,7 @@ reorderable = "sh.calvin.reorderable:reorderable:2.4.0"
serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinx-serialization" }
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "kotlinx-serialization" }
serialization-json-io = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-io", version.ref = "kotlinx-serialization" }
serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization", version.ref = "xmlutil" }
@@ -126,7 +126,7 @@ androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstall
arrow = ["arrow-fx-coroutines", "arrow-functions", "arrow-resilience"]
coil = ["coil-compose", "coil-gif", "coil-network"]
compose = ["compose-foundation", "compose-material-icons-extended", "compose-material3", "compose-material3-adaptive", "compose-ui-util"]
kotlinx-serialization = ["serialization-cbor", "serialization-json", "serialization-json-okio", "serialization-xml"]
kotlinx-serialization = ["serialization-cbor", "serialization-json", "serialization-json-io", "serialization-xml"]
splitties = ["splitties-appctx", "splitties-arch-room", "splitties-preferences", "splitties-systemservices"]
[plugins]