Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decouples Cache and Download behaviour #493

Merged
merged 7 commits into from
Feb 12, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SortOrder.ASC
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.manga.impl.Manga.getManga
import suwayomi.tachidesk.manga.impl.util.getChapterDir
Expand Down Expand Up @@ -312,9 +311,7 @@ object Chapter {
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }
.first()[ChapterTable.id].value

val chapterDir = getChapterDir(mangaId, chapterId)

File(chapterDir).deleteRecursively()
ChapterDownloadHelper.delete(mangaId, chapterId)

ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }) {
it[isDownloaded] = false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package suwayomi.tachidesk.manga.impl

import suwayomi.tachidesk.manga.impl.download.DownloadedFilesProvider
import suwayomi.tachidesk.manga.impl.download.FolderProvider
import java.io.InputStream

object ChapterDownloadHelper {
fun getImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, String> {
return provider(mangaId, chapterId).getImage(index)
}

fun delete(mangaId: Int, chapterId: Int): Boolean {
return provider(mangaId, chapterId).delete()
}

fun putImage(mangaId: Int, chapterId: Int, index: Int, image: InputStream): Boolean {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

going to change this to download since we don't need a single put image here, we can just move the entire downlod logic here. Also, building an ArchiveProvider is proving difficult since appending to zip is not working for some reason

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this change in the latest commit

return provider(mangaId, chapterId).putImage(index, image)
}

// return the appropriate provider based on how the download was saved. For the logic is simple but will evolve when new types of downloads are available
private fun provider(mangaId: Int, chapterId: Int): DownloadedFilesProvider {
return FolderProvider(mangaId, chapterId)
}
}
9 changes: 5 additions & 4 deletions server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Page.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.util.getChapterDir
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
Expand Down Expand Up @@ -82,11 +81,13 @@ object Page {
}
}

val chapterDir = getChapterDir(mangaId, chapterId)
File(chapterDir).mkdirs()
val fileName = getPageName(index)

return getImageResponse(chapterDir, fileName, useCache) {
if (chapterEntry[ChapterTable.isDownloaded]) {
return ChapterDownloadHelper.getImage(mangaId, chapterId, index)
}

return getImageResponse(mangaId, chapterId, fileName, useCache) {
source.fetchImage(tachiyomiPage).awaitSingle()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package suwayomi.tachidesk.manga.impl.download

import java.io.InputStream

/*
* Base class for downloaded chapter files provider, example: Folder, Archive
* */
abstract class DownloadedFilesProvider(val mangaId: Int, val chapterId: Int) {
abstract fun getImage(index: Int): Pair<InputStream, String>

abstract fun putImage(index: Int, image: InputStream): Boolean

abstract fun delete(): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import mu.KotlinLogging
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.ChapterDownloadHelper
import suwayomi.tachidesk.manga.impl.Page.getPageImage
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReady
import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter
Expand Down Expand Up @@ -99,7 +100,7 @@ class Downloader(
for (pageNum in 0 until pageCount) {
var pageProgressJob: Job? = null
try {
getPageImage(
val image = getPageImage(
mangaId = download.mangaId,
chapterIndex = download.chapterIndex,
index = pageNum,
Expand All @@ -113,7 +114,9 @@ class Downloader(
}
.launchIn(scope)
}
).first.close()
).first
ChapterDownloadHelper.putImage(download.mangaId, download.chapter.id, pageNum, image)
image.close()
} finally {
// always cancel the page progress job even if it throws an exception to avoid memory leaks
pageProgressJob?.cancel()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package suwayomi.tachidesk.manga.impl.download

import suwayomi.tachidesk.manga.impl.Page.getPageName
import suwayomi.tachidesk.manga.impl.util.getChapterDir
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
import java.io.File
import java.io.FileInputStream
import java.io.InputStream

/*
* Provides downloaded files when pages were downloaded into folders
* */
class FolderProvider(mangaId: Int, chapterId: Int) : DownloadedFilesProvider(mangaId, chapterId) {
override fun getImage(index: Int): Pair<InputStream, String> {
val chapterDir = getChapterDir(mangaId, chapterId)
val folder = File(chapterDir)
folder.mkdirs()
val file = folder.listFiles()?.get(index)
val fileType = file!!.name.substringAfterLast(".")
return Pair(FileInputStream(file).buffered(), "image/$fileType")
}

override fun putImage(index: Int, image: InputStream): Boolean {
val chapterDir = getChapterDir(mangaId, chapterId)
val folder = File(chapterDir)
folder.mkdirs()
val fileName = getPageName(index)
val filePath = "$chapterDir/$fileName"
ImageResponse.saveImage(filePath, image)
return true
}

override fun delete(): Boolean {
val chapterDir = getChapterDir(mangaId, chapterId)
return File(chapterDir).deleteRecursively()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package suwayomi.tachidesk.manga.impl.util
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */

import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.kodein.di.DI
Expand All @@ -21,17 +22,16 @@ import java.io.File

private val applicationDirs by DI.global.instance<ApplicationDirs>()

fun getMangaDir(mangaId: Int): String {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
fun getMangaDir(mangaId: Int, cache: Boolean = false): String {
val mangaEntry = getMangaEntry(mangaId)
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])

val sourceDir = source.toString()
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title])

return "${applicationDirs.mangaDownloadsRoot}/$sourceDir/$mangaDir"
return (if (cache) applicationDirs.cacheRoot else applicationDirs.mangaDownloadsRoot) + "/$sourceDir/$mangaDir"
}

fun getChapterDir(mangaId: Int, chapterId: Int): String {
fun getChapterDir(mangaId: Int, chapterId: Int, cache: Boolean = false): String {
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.first() }

val chapterDir = SafePath.buildValidFilename(
Expand All @@ -41,12 +41,12 @@ fun getChapterDir(mangaId: Int, chapterId: Int): String {
}
)

return getMangaDir(mangaId) + "/$chapterDir"
return getMangaDir(mangaId, cache) + "/$chapterDir"
}

/** return value says if rename/move was successful */
fun updateMangaDownloadDir(mangaId: Int, newTitle: String): Boolean {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val mangaEntry = getMangaEntry(mangaId)
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])

val sourceDir = source.toString()
Expand All @@ -66,3 +66,7 @@ fun updateMangaDownloadDir(mangaId: Int, newTitle: String): Boolean {
true
}
}

private fun getMangaEntry(mangaId: Int): ResultRow {
return transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package suwayomi.tachidesk.manga.impl.util.storage

import okhttp3.Response
import okhttp3.internal.closeQuietly
import suwayomi.tachidesk.manga.impl.util.getChapterDir
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
Expand All @@ -29,7 +30,7 @@ object ImageResponse {
}

/** fetch a cached image response, calls `fetcher` if cache fails */
suspend fun getCachedImageResponse(saveDir: String, fileName: String, fetcher: suspend () -> Response): Pair<InputStream, String> {
private suspend fun getCachedImageResponse(saveDir: String, fileName: String, fetcher: suspend () -> Response): Pair<InputStream, String> {
val cachedFile = findFileNameStartingWith(saveDir, fileName)
val filePath = "$saveDir/$fileName"
if (cachedFile != null) {
Expand All @@ -43,34 +44,37 @@ object ImageResponse {
val response = fetcher()

if (response.code == 200) {
val tmpSavePath = "$filePath.tmp"
val tmpSaveFile = File(tmpSavePath)
response.body!!.source().saveTo(tmpSaveFile)

// find image type
val imageType = response.headers["content-type"]
?: ImageUtil.findImageType { tmpSaveFile.inputStream() }?.mime
?: "image/jpeg"

val actualSavePath = "$filePath.${imageType.substringAfter("/")}"

tmpSaveFile.renameTo(File(actualSavePath))

val (actualSavePath, imageType) = saveImage(filePath, response.body!!.byteStream())
return pathToInputStream(actualSavePath) to imageType
} else {
response.closeQuietly()
throw Exception("request error! ${response.code}")
}
}

fun saveImage(filePath: String, image: InputStream): Pair<String, String> {
val tmpSavePath = "$filePath.tmp"
val tmpSaveFile = File(tmpSavePath)
image.use { input -> tmpSaveFile.outputStream().use { output -> input.copyTo(output) } }

// find image type
val imageType = ImageUtil.findImageType { tmpSaveFile.inputStream() }?.mime
?: "image/jpeg"

val actualSavePath = "$filePath.${imageType.substringAfter("/")}"

tmpSaveFile.renameTo(File(actualSavePath))
return Pair(actualSavePath, imageType)
}

fun clearCachedImage(saveDir: String, fileName: String) {
val cachedFile = findFileNameStartingWith(saveDir, fileName)
cachedFile?.also {
File(it).delete()
}
}

suspend fun getNoCacheImageResponse(fetcher: suspend () -> Response): Pair<InputStream, String> {
private suspend fun getNoCacheImageResponse(fetcher: suspend () -> Response): Pair<InputStream, String> {
val response = fetcher()

if (response.code == 200) {
Expand All @@ -88,11 +92,20 @@ object ImageResponse {
}
}

suspend fun getImageResponse(saveDir: String, fileName: String, useCache: Boolean = false, fetcher: suspend () -> Response): Pair<InputStream, String> {
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ private val logger = KotlinLogging.logger {}
class ApplicationDirs(
val dataRoot: String = ApplicationRootDir
) {
val cacheRoot = System.getProperty("java.io.tmpdir") + "/tachidesk"
val extensionsRoot = "$dataRoot/extensions"
val thumbnailsRoot = "$dataRoot/thumbnails"
val mangaDownloadsRoot = serverConfig.downloadsPath.ifBlank { "$dataRoot/downloads" }
Expand Down