rethink image cache (#498)

This commit is contained in:
Aria Moradi
2023-02-12 18:33:36 +03:30
committed by GitHub
parent b10062c73d
commit 54bbb5e384
11 changed files with 64 additions and 80 deletions

View File

@@ -15,7 +15,6 @@ import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.queryParam
import suwayomi.tachidesk.server.util.withOperation import suwayomi.tachidesk.server.util.withOperation
object ExtensionController { object ExtensionController {
@@ -141,16 +140,15 @@ object ExtensionController {
/** icon for extension named `apkName` */ /** icon for extension named `apkName` */
val icon = handler( val icon = handler(
pathParam<String>("apkName"), pathParam<String>("apkName"),
queryParam("useCache", true),
documentWith = { documentWith = {
withOperation { withOperation {
summary("Extension icon") summary("Extension icon")
description("Icon for extension named `apkName`") description("Icon for extension named `apkName`")
} }
}, },
behaviorOf = { ctx, apkName, useCache -> behaviorOf = { ctx, apkName ->
ctx.future( ctx.future(
future { Extension.getExtensionIcon(apkName, useCache) } future { Extension.getExtensionIcon(apkName) }
.thenApply { .thenApply {
ctx.header("content-type", it.second) ctx.header("content-type", it.second)
it.first it.first

View File

@@ -81,16 +81,15 @@ object MangaController {
/** manga thumbnail */ /** manga thumbnail */
val thumbnail = handler( val thumbnail = handler(
pathParam<Int>("mangaId"), pathParam<Int>("mangaId"),
queryParam("useCache", true),
documentWith = { documentWith = {
withOperation { withOperation {
summary("Get a manga thumbnail") summary("Get a manga thumbnail")
description("Get a manga thumbnail from the source or the cache.") description("Get a manga thumbnail from the source or the cache.")
} }
}, },
behaviorOf = { ctx, mangaId, useCache -> behaviorOf = { ctx, mangaId ->
ctx.future( ctx.future(
future { Manga.getMangaThumbnail(mangaId, useCache) } future { Manga.getMangaThumbnail(mangaId) }
.thenApply { .thenApply {
ctx.header("content-type", it.second) ctx.header("content-type", it.second)
val httpCacheSeconds = 1.days.inWholeSeconds val httpCacheSeconds = 1.days.inWholeSeconds
@@ -375,16 +374,15 @@ object MangaController {
pathParam<Int>("mangaId"), pathParam<Int>("mangaId"),
pathParam<Int>("chapterIndex"), pathParam<Int>("chapterIndex"),
pathParam<Int>("index"), pathParam<Int>("index"),
queryParam("useCache", true),
documentWith = { documentWith = {
withOperation { withOperation {
summary("Get a chapter page") summary("Get a chapter page")
description("Get a chapter page for a given index. Cache use can be disabled so it only retrieves it directly from the source.") description("Get a chapter page for a given index. Cache use can be disabled so it only retrieves it directly from the source.")
} }
}, },
behaviorOf = { ctx, mangaId, chapterIndex, index, useCache -> behaviorOf = { ctx, mangaId, chapterIndex, index ->
ctx.future( ctx.future(
future { Page.getPageImage(mangaId, chapterIndex, index, useCache) } future { Page.getPageImage(mangaId, chapterIndex, index) }
.thenApply { .thenApply {
ctx.header("content-type", it.second) ctx.header("content-type", it.second)
val httpCacheSeconds = 1.days.inWholeSeconds val httpCacheSeconds = 1.days.inWholeSeconds

View File

@@ -23,7 +23,6 @@ import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.Manga.getManga import suwayomi.tachidesk.manga.impl.Manga.getManga
import suwayomi.tachidesk.manga.impl.util.getChapterDir
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
@@ -36,7 +35,6 @@ import suwayomi.tachidesk.manga.model.table.ChapterTable.scanlator
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.PageTable import suwayomi.tachidesk.manga.model.table.PageTable
import suwayomi.tachidesk.manga.model.table.toDataClass import suwayomi.tachidesk.manga.model.table.toDataClass
import java.io.File
import java.time.Instant import java.time.Instant
object Chapter { object Chapter {
@@ -345,8 +343,7 @@ object Chapter {
.forEach { row -> .forEach { row ->
val chapterMangaId = row[ChapterTable.manga].value val chapterMangaId = row[ChapterTable.manga].value
val chapterId = row[ChapterTable.id].value val chapterId = row[ChapterTable.id].value
val chapterDir = getChapterDir(chapterMangaId, chapterId) ChapterDownloadHelper.delete(chapterMangaId, chapterId)
File(chapterDir).deleteRecursively()
} }
ChapterTable.update({ ChapterTable.id inList chapterIds }) { ChapterTable.update({ ChapterTable.id inList chapterIds }) {
@@ -359,8 +356,8 @@ object Chapter {
.select { (ChapterTable.sourceOrder inList input.chapterIndexes) and (ChapterTable.manga eq mangaId) } .select { (ChapterTable.sourceOrder inList input.chapterIndexes) and (ChapterTable.manga eq mangaId) }
.map { row -> .map { row ->
val chapterId = row[ChapterTable.id].value val chapterId = row[ChapterTable.id].value
val chapterDir = getChapterDir(mangaId, chapterId) ChapterDownloadHelper.delete(mangaId, chapterId)
File(chapterDir).deleteRecursively()
chapterId chapterId
} }

View File

@@ -31,7 +31,7 @@ import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogue
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.source.StubSource import suwayomi.tachidesk.manga.impl.util.source.StubSource
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getCachedImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import suwayomi.tachidesk.manga.impl.util.updateMangaDownloadDir import suwayomi.tachidesk.manga.impl.util.updateMangaDownloadDir
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
@@ -91,7 +91,7 @@ object Manga {
if (!sManga.thumbnail_url.isNullOrEmpty() && sManga.thumbnail_url != mangaEntry[MangaTable.thumbnail_url]) { if (!sManga.thumbnail_url.isNullOrEmpty() && sManga.thumbnail_url != mangaEntry[MangaTable.thumbnail_url]) {
it[MangaTable.thumbnail_url] = sManga.thumbnail_url it[MangaTable.thumbnail_url] = sManga.thumbnail_url
it[MangaTable.thumbnailUrlLastFetched] = Instant.now().epochSecond it[MangaTable.thumbnailUrlLastFetched] = Instant.now().epochSecond
clearMangaThumbnail(mangaId) clearMangaThumbnailCache(mangaId)
} }
it[MangaTable.realUrl] = runCatching { it[MangaTable.realUrl] = runCatching {
@@ -225,15 +225,15 @@ object Manga {
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
private val network: NetworkHelper by injectLazy() private val network: NetworkHelper by injectLazy()
suspend fun getMangaThumbnail(mangaId: Int, useCache: Boolean): Pair<InputStream, String> { suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
val saveDir = applicationDirs.thumbnailsRoot val cacheSaveDir = applicationDirs.thumbnailsRoot
val fileName = mangaId.toString() val fileName = mangaId.toString()
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val sourceId = mangaEntry[MangaTable.sourceReference] val sourceId = mangaEntry[MangaTable.sourceReference]
return when (val source = getCatalogueSourceOrStub(sourceId)) { return when (val source = getCatalogueSourceOrStub(sourceId)) {
is HttpSource -> getImageResponse(saveDir, fileName, useCache) { is HttpSource -> getCachedImageResponse(cacheSaveDir, fileName) {
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url] val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
?: if (!mangaEntry[MangaTable.initialized]) { ?: if (!mangaEntry[MangaTable.initialized]) {
// initialize then try again // initialize then try again
@@ -265,7 +265,7 @@ object Manga {
imageFile.inputStream() to contentType imageFile.inputStream() to contentType
} }
is StubSource -> getImageResponse(saveDir, fileName, useCache) { is StubSource -> getCachedImageResponse(cacheSaveDir, fileName) {
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url] val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
?: throw NullPointerException("No thumbnail found") ?: throw NullPointerException("No thumbnail found")
network.client.newCall( network.client.newCall(
@@ -277,7 +277,7 @@ object Manga {
} }
} }
private fun clearMangaThumbnail(mangaId: Int) { private fun clearMangaThumbnailCache(mangaId: Int) {
val saveDir = applicationDirs.thumbnailsRoot val saveDir = applicationDirs.thumbnailsRoot
val fileName = mangaId.toString() val fileName = mangaId.toString()

View File

@@ -15,9 +15,10 @@ import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getCachedImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
@@ -37,7 +38,7 @@ object Page {
return page.imageUrl!! return page.imageUrl!!
} }
suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int, useCache: Boolean = true, progressFlow: ((StateFlow<Int>) -> Unit)? = null): Pair<InputStream, String> { suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int, progressFlow: ((StateFlow<Int>) -> Unit)? = null): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference]) val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val chapterEntry = transaction { val chapterEntry = transaction {
@@ -87,7 +88,10 @@ object Page {
return ChapterDownloadHelper.getImage(mangaId, chapterId, index) return ChapterDownloadHelper.getImage(mangaId, chapterId, index)
} }
return getImageResponse(mangaId, chapterId, fileName, useCache) { val cacheSaveDir = getChapterCachePath(mangaId, chapterId)
// Note: don't care about invalidating cache because OS cache is not permanent
return getCachedImageResponse(cacheSaveDir, fileName) {
source.fetchImage(tachiyomiPage).awaitSingle() source.fetchImage(tachiyomiPage).awaitSingle()
} }
} }

View File

@@ -16,7 +16,7 @@ import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.Page.getPageName import suwayomi.tachidesk.manga.impl.Page.getPageName
import suwayomi.tachidesk.manga.impl.util.getChapterDir import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
@@ -133,7 +133,7 @@ private class ChapterForDownload(
private fun firstPageExists(): Boolean { private fun firstPageExists(): Boolean {
val chapterId = chapterEntry[ChapterTable.id].value val chapterId = chapterEntry[ChapterTable.id].value
val chapterDir = getChapterDir(mangaId, chapterId) val chapterDir = getChapterDownloadPath(mangaId, chapterId)
println(chapterDir) println(chapterDir)
println(getPageName(0)) println(getPageName(0))

View File

@@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.sample
import suwayomi.tachidesk.manga.impl.Page import suwayomi.tachidesk.manga.impl.Page
import suwayomi.tachidesk.manga.impl.Page.getPageName import suwayomi.tachidesk.manga.impl.Page.getPageName
import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter
import suwayomi.tachidesk.manga.impl.util.getChapterDir import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
@@ -21,7 +21,7 @@ import java.io.InputStream
* */ * */
class FolderProvider(mangaId: Int, chapterId: Int) : DownloadedFilesProvider(mangaId, chapterId) { class FolderProvider(mangaId: Int, chapterId: Int) : DownloadedFilesProvider(mangaId, chapterId) {
override fun getImage(index: Int): Pair<InputStream, String> { override fun getImage(index: Int): Pair<InputStream, String> {
val chapterDir = getChapterDir(mangaId, chapterId) val chapterDir = getChapterDownloadPath(mangaId, chapterId)
val folder = File(chapterDir) val folder = File(chapterDir)
folder.mkdirs() folder.mkdirs()
val file = folder.listFiles()?.get(index) val file = folder.listFiles()?.get(index)
@@ -36,7 +36,7 @@ class FolderProvider(mangaId: Int, chapterId: Int) : DownloadedFilesProvider(man
step: suspend (DownloadChapter?, Boolean) -> Unit step: suspend (DownloadChapter?, Boolean) -> Unit
): Boolean { ): Boolean {
val pageCount = download.chapter.pageCount val pageCount = download.chapter.pageCount
val chapterDir = getChapterDir(mangaId, chapterId) val chapterDir = getChapterDownloadPath(mangaId, chapterId)
val folder = File(chapterDir) val folder = File(chapterDir)
folder.mkdirs() folder.mkdirs()
@@ -74,7 +74,7 @@ class FolderProvider(mangaId: Int, chapterId: Int) : DownloadedFilesProvider(man
} }
override fun delete(): Boolean { override fun delete(): Boolean {
val chapterDir = getChapterDir(mangaId, chapterId) val chapterDir = getChapterDownloadPath(mangaId, chapterId)
return File(chapterDir).deleteRecursively() return File(chapterDir).deleteRecursively()
} }

View File

@@ -40,7 +40,7 @@ import suwayomi.tachidesk.manga.impl.util.PackageTools.getPackageInfo
import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources
import suwayomi.tachidesk.manga.impl.util.network.await import suwayomi.tachidesk.manga.impl.util.network.await
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getCachedImageResponse
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.manga.model.table.SourceTable
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
@@ -266,16 +266,16 @@ object Extension {
return installExtension(pkgName) return installExtension(pkgName)
} }
suspend fun getExtensionIcon(apkName: String, useCache: Boolean): Pair<InputStream, String> { suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
val iconUrl = if (apkName == "localSource") { val iconUrl = if (apkName == "localSource") {
"" ""
} else { } else {
transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.first() }[ExtensionTable.iconUrl] transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.first() }[ExtensionTable.iconUrl]
} }
val saveDir = "${applicationDirs.extensionsRoot}/icon" val cacheSaveDir = "${applicationDirs.extensionsRoot}/icon"
return getImageResponse(saveDir, apkName, useCache) { return getCachedImageResponse(cacheSaveDir, apkName) {
network.client.newCall( network.client.newCall(
GET(iconUrl) GET(iconUrl)
).await() ).await()

View File

@@ -22,16 +22,16 @@ import java.io.File
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
fun getMangaDir(mangaId: Int, cache: Boolean = false): String { private fun getMangaDir(mangaId: Int): String {
val mangaEntry = getMangaEntry(mangaId) val mangaEntry = getMangaEntry(mangaId)
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference]) val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val sourceDir = source.toString() val sourceDir = SafePath.buildValidFilename(source.toString())
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title]) val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title])
return (if (cache) applicationDirs.cacheRoot else applicationDirs.mangaDownloadsRoot) + "/$sourceDir/$mangaDir" return "$sourceDir/$mangaDir"
} }
fun getChapterDir(mangaId: Int, chapterId: Int, cache: Boolean = false): String { private fun getChapterDir(mangaId: Int, chapterId: Int): String {
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.first() } val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.first() }
val chapterDir = SafePath.buildValidFilename( val chapterDir = SafePath.buildValidFilename(
@@ -41,9 +41,18 @@ fun getChapterDir(mangaId: Int, chapterId: Int, cache: Boolean = false): String
} }
) )
return getMangaDir(mangaId, cache) + "/$chapterDir" return getMangaDir(mangaId) + "/$chapterDir"
} }
fun getChapterDownloadPath(mangaId: Int, chapterId: Int): String {
return applicationDirs.mangaDownloadsRoot + "/" + getChapterDir(mangaId, chapterId)
}
fun getChapterCachePath(mangaId: Int, chapterId: Int): String {
return applicationDirs.tempMangaCacheRoot + "/" + getChapterDir(mangaId, chapterId)
}
// (if (useTempCache) applicationDirs.tempCacheRoot else )
/** return value says if rename/move was successful */ /** return value says if rename/move was successful */
fun updateMangaDownloadDir(mangaId: Int, newTitle: String): Boolean { fun updateMangaDownloadDir(mangaId: Int, newTitle: String): Boolean {
val mangaEntry = getMangaEntry(mangaId) val mangaEntry = getMangaEntry(mangaId)

View File

@@ -9,7 +9,6 @@ package suwayomi.tachidesk.manga.impl.util.storage
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import suwayomi.tachidesk.manga.impl.util.getChapterDir
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStream import java.io.InputStream
@@ -19,6 +18,7 @@ object ImageResponse {
return FileInputStream(path).buffered() return FileInputStream(path).buffered()
} }
/** find file with name when file extension is not known */
fun findFileNameStartingWith(directoryPath: String, fileName: String): String? { fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
val target = "$fileName." val target = "$fileName."
File(directoryPath).listFiles().orEmpty().forEach { file -> File(directoryPath).listFiles().orEmpty().forEach { file ->
@@ -29,8 +29,17 @@ object ImageResponse {
return null return null
} }
/** fetch a cached image response, calls `fetcher` if cache fails */ /**
private suspend fun getCachedImageResponse(saveDir: String, fileName: String, fetcher: suspend () -> Response): Pair<InputStream, String> { * Get a cached image response
*
* Note: The caller should also call [clearCachedImage] when appropriate
*
* @param cacheSavePath where to save the cached image. Caller should decide to use perma cache or temp cache (OS temp dir)
* @param fileName what the saved cache file should be named
*/
suspend fun getCachedImageResponse(saveDir: String, fileName: String, fetcher: suspend () -> Response): Pair<InputStream, String> {
File(saveDir).mkdirs()
val cachedFile = findFileNameStartingWith(saveDir, fileName) val cachedFile = findFileNameStartingWith(saveDir, fileName)
val filePath = "$saveDir/$fileName" val filePath = "$saveDir/$fileName"
if (cachedFile != null) { if (cachedFile != null) {
@@ -52,6 +61,7 @@ object ImageResponse {
} }
} }
/** Save image safely */
fun saveImage(filePath: String, image: InputStream): Pair<String, String> { fun saveImage(filePath: String, image: InputStream): Pair<String, String> {
val tmpSavePath = "$filePath.tmp" val tmpSavePath = "$filePath.tmp"
val tmpSaveFile = File(tmpSavePath) val tmpSaveFile = File(tmpSavePath)
@@ -73,39 +83,4 @@ object ImageResponse {
File(it).delete() File(it).delete()
} }
} }
private suspend fun getNoCacheImageResponse(fetcher: suspend () -> Response): Pair<InputStream, String> {
val response = fetcher()
if (response.code == 200) {
val responseBytes = response.body!!.bytes()
// find image type
val imageType = response.headers["content-type"]
?: ImageUtil.findImageType { responseBytes.inputStream() }?.mime
?: "image/jpeg"
return responseBytes.inputStream() to imageType
} else {
response.closeQuietly()
throw Exception("request error! ${response.code}")
}
}
suspend fun getImageResponse(saveDir: String, fileName: String, useCache: Boolean, fetcher: suspend () -> Response): Pair<InputStream, String> {
return if (useCache) {
getCachedImageResponse(saveDir, fileName, fetcher)
} else {
getNoCacheImageResponse(fetcher)
}
}
suspend fun getImageResponse(mangaId: Int, chapterId: Int, fileName: String, useCache: Boolean, fetcher: suspend () -> Response): Pair<InputStream, String> {
var saveDir = ""
if (useCache) {
saveDir = getChapterDir(mangaId, chapterId, true)
File(saveDir).mkdirs()
}
return getImageResponse(saveDir, fileName, useCache, fetcher)
}
} }

View File

@@ -36,7 +36,8 @@ import java.util.Locale
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
class ApplicationDirs( class ApplicationDirs(
val dataRoot: String = ApplicationRootDir val dataRoot: String = ApplicationRootDir,
val tempRoot: String = "${System.getProperty("java.io.tmpdir")}/Tachidesk"
) { ) {
val cacheRoot = System.getProperty("java.io.tmpdir") + "/tachidesk" val cacheRoot = System.getProperty("java.io.tmpdir") + "/tachidesk"
val extensionsRoot = "$dataRoot/extensions" val extensionsRoot = "$dataRoot/extensions"
@@ -44,6 +45,8 @@ class ApplicationDirs(
val mangaDownloadsRoot = serverConfig.downloadsPath.ifBlank { "$dataRoot/downloads" } val mangaDownloadsRoot = serverConfig.downloadsPath.ifBlank { "$dataRoot/downloads" }
val localMangaRoot = "$dataRoot/local" val localMangaRoot = "$dataRoot/local"
val webUIRoot = "$dataRoot/webUI" val webUIRoot = "$dataRoot/webUI"
val tempMangaCacheRoot = "$tempRoot/manga-cache"
} }
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() } val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }