diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt index 36b4b45b..b30c0f8c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt @@ -48,6 +48,7 @@ import suwayomi.tachidesk.manga.model.dataclass.TrackRecordDataClass import suwayomi.tachidesk.manga.model.table.CategoryTable import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.server.database.dbTransaction import java.io.InputStream import java.util.Date import java.util.Timer @@ -56,6 +57,11 @@ import java.util.concurrent.TimeUnit import kotlin.math.max import suwayomi.tachidesk.manga.impl.track.Track as Tracker +enum class RestoreMode { + NEW, + EXISTING, +} + object ProtoBackupImport : ProtoBackupBase() { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @@ -312,32 +318,10 @@ object ProtoBackupImport : ProtoBackupBase() { clearThumbnail(mangaId) // insert chapter data - val chaptersLength = chapters.size - ChapterTable.batchInsert(chapters) { chapter -> - this[ChapterTable.url] = chapter.url - this[ChapterTable.name] = chapter.name - if (chapter.date_upload == 0L) { - this[ChapterTable.date_upload] = chapter.date_fetch - } else { - this[ChapterTable.date_upload] = chapter.date_upload - } - this[ChapterTable.chapter_number] = chapter.chapter_number - this[ChapterTable.scanlator] = chapter.scanlator - - this[ChapterTable.sourceOrder] = chaptersLength - chapter.source_order - this[ChapterTable.manga] = mangaId - - this[ChapterTable.isRead] = chapter.read - this[ChapterTable.lastPageRead] = chapter.last_page_read - this[ChapterTable.isBookmarked] = chapter.bookmark - - this[ChapterTable.fetchedAt] = TimeUnit.MILLISECONDS.toSeconds(chapter.date_fetch) - } + restoreMangaChapterData(mangaId, RestoreMode.NEW, chapters) // insert categories - categories.forEach { backupCategoryOrder -> - CategoryManga.addMangaToCategory(mangaId, categoryMapping[backupCategoryOrder]!!) - } + restoreMangaCategoryData(mangaId, categories, categoryMapping) mangaId } @@ -363,49 +347,100 @@ object ProtoBackupImport : ProtoBackupBase() { } // merge chapter data - val chaptersLength = chapters.size - val dbChapters = ChapterTable.selectAll().where { ChapterTable.manga eq mangaId } - - chapters.forEach { chapter -> - val dbChapter = dbChapters.find { it[ChapterTable.url] == chapter.url } - - if (dbChapter == null) { - ChapterTable.insert { - it[url] = chapter.url - it[name] = chapter.name - if (chapter.date_upload == 0L) { - it[date_upload] = chapter.date_fetch - } else { - it[date_upload] = chapter.date_upload - } - it[chapter_number] = chapter.chapter_number - it[scanlator] = chapter.scanlator - - it[sourceOrder] = chaptersLength - chapter.source_order - it[ChapterTable.manga] = mangaId - - it[isRead] = chapter.read - it[lastPageRead] = chapter.last_page_read - it[isBookmarked] = chapter.bookmark - } - } else { - ChapterTable.update({ (ChapterTable.url eq dbChapter[ChapterTable.url]) and (ChapterTable.manga eq mangaId) }) { - it[isRead] = chapter.read || dbChapter[isRead] - it[lastPageRead] = max(chapter.last_page_read, dbChapter[lastPageRead]) - it[isBookmarked] = chapter.bookmark || dbChapter[isBookmarked] - } - } - } + restoreMangaChapterData(mangaId, RestoreMode.EXISTING, chapters) // merge categories - categories.forEach { backupCategoryOrder -> - CategoryManga.addMangaToCategory(mangaId, categoryMapping[backupCategoryOrder]!!) - } + restoreMangaCategoryData(mangaId, categories, categoryMapping) mangaId } } + restoreMangaTrackerData(mangaId, tracks) + + // TODO: insert/merge history + } + + private fun restoreMangaChapterData( + mangaId: Int, + restoreMode: RestoreMode, + chapters: List, + ) = dbTransaction { + val uniqueChapters = chapters.distinctBy { it.url } + val chaptersLength = uniqueChapters.size + + if (restoreMode == RestoreMode.NEW) { + ChapterTable.batchInsert(uniqueChapters) { chapter -> + this[ChapterTable.url] = chapter.url + this[ChapterTable.name] = chapter.name + if (chapter.date_upload == 0L) { + this[ChapterTable.date_upload] = chapter.date_fetch + } else { + this[ChapterTable.date_upload] = chapter.date_upload + } + this[ChapterTable.chapter_number] = chapter.chapter_number + this[ChapterTable.scanlator] = chapter.scanlator + + this[ChapterTable.sourceOrder] = chaptersLength - chapter.source_order + this[ChapterTable.manga] = mangaId + + this[ChapterTable.isRead] = chapter.read + this[ChapterTable.lastPageRead] = chapter.last_page_read + this[ChapterTable.isBookmarked] = chapter.bookmark + + this[ChapterTable.fetchedAt] = TimeUnit.MILLISECONDS.toSeconds(chapter.date_fetch) + } + } + + // merge chapter data + val dbChapters = ChapterTable.selectAll().where { ChapterTable.manga eq mangaId } + + uniqueChapters.forEach { chapter -> + val dbChapter = dbChapters.find { it[ChapterTable.url] == chapter.url } + + if (dbChapter == null) { + ChapterTable.insert { + it[url] = chapter.url + it[name] = chapter.name + if (chapter.date_upload == 0L) { + it[date_upload] = chapter.date_fetch + } else { + it[date_upload] = chapter.date_upload + } + it[chapter_number] = chapter.chapter_number + it[scanlator] = chapter.scanlator + + it[sourceOrder] = chaptersLength - chapter.source_order + it[ChapterTable.manga] = mangaId + + it[isRead] = chapter.read + it[lastPageRead] = chapter.last_page_read + it[isBookmarked] = chapter.bookmark + } + } else { + ChapterTable.update({ (ChapterTable.url eq dbChapter[ChapterTable.url]) and (ChapterTable.manga eq mangaId) }) { + it[isRead] = chapter.read || dbChapter[isRead] + it[lastPageRead] = max(chapter.last_page_read, dbChapter[lastPageRead]) + it[isBookmarked] = chapter.bookmark || dbChapter[isBookmarked] + } + } + } + } + + private fun restoreMangaCategoryData( + mangaId: Int, + categories: List, + categoryMapping: Map, + ) { + categories.forEach { backupCategoryOrder -> + CategoryManga.addMangaToCategory(mangaId, categoryMapping[backupCategoryOrder]!!) + } + } + + private fun restoreMangaTrackerData( + mangaId: Int, + tracks: List, + ) { val dbTrackRecordsByTrackerId = Tracker .getTrackRecordsByMangaId(mangaId) @@ -440,8 +475,6 @@ object ProtoBackupImport : ProtoBackupBase() { existingTracks.forEach(Tracker::updateTrackRecord) newTracks.forEach(Tracker::insertTrackRecord) - - // TODO: insert/merge history } private fun TrackRecordDataClass.forComparison() = this.copy(id = 0, mangaId = 0) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/database/DBTransaction.util.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/database/DBTransaction.util.kt new file mode 100644 index 00000000..5c475df2 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/database/DBTransaction.util.kt @@ -0,0 +1,33 @@ +package suwayomi.tachidesk.server.database + +import org.jetbrains.exposed.sql.Transaction +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.jetbrains.exposed.sql.transactions.transaction + +/** + * Performs the given transaction block inside the parent transaction or creates a new transaction if necessary. + * + * Any rollback or exception in the inner transaction will be propagated to the parent transaction. + */ +fun dbTransaction(block: Transaction.() -> T): T { + val currentTransaction = TransactionManager.currentOrNull() + + return if (currentTransaction == null) { + transaction { block() } + } else { + block(currentTransaction) + } +} + +/** + * Creates a nested transaction. + * + * Any rollback or exception will only roll back the inner (nested) transaction, leaving the parent transaction unaffected. + * + * Only works in case "useNestedTransactions" is enabled. + */ +fun nestedDbTransaction(block: Transaction.() -> T): T = + transaction { + check(db.useNestedTransactions) { "Nested transactions are not enabled." } + block() + }