Feature/backup import add backup flags (#1697)
* Add backup flags to backup restore * Cleanup default backup flags handling * Optionally exclude manga from backup
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
|
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||||
import io.javalin.http.UploadedFile
|
import io.javalin.http.UploadedFile
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
import suwayomi.tachidesk.graphql.server.TemporaryFileStorage
|
import suwayomi.tachidesk.graphql.server.TemporaryFileStorage
|
||||||
import suwayomi.tachidesk.graphql.types.BackupRestoreStatus
|
import suwayomi.tachidesk.graphql.types.BackupRestoreStatus
|
||||||
|
import suwayomi.tachidesk.graphql.types.PartialBackupFlags
|
||||||
import suwayomi.tachidesk.graphql.types.toStatus
|
import suwayomi.tachidesk.graphql.types.toStatus
|
||||||
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
|
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
|
||||||
@@ -19,6 +21,7 @@ class BackupMutation {
|
|||||||
data class RestoreBackupInput(
|
data class RestoreBackupInput(
|
||||||
val clientMutationId: String? = null,
|
val clientMutationId: String? = null,
|
||||||
val backup: UploadedFile,
|
val backup: UploadedFile,
|
||||||
|
val flags: PartialBackupFlags? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class RestoreBackupPayload(
|
data class RestoreBackupPayload(
|
||||||
@@ -29,10 +32,14 @@ class BackupMutation {
|
|||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
fun restoreBackup(input: RestoreBackupInput): CompletableFuture<RestoreBackupPayload> {
|
fun restoreBackup(input: RestoreBackupInput): CompletableFuture<RestoreBackupPayload> {
|
||||||
val (clientMutationId, backup) = input
|
val (clientMutationId, backup, flags) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
val restoreId = ProtoBackupImport.restore(backup.content())
|
val restoreId =
|
||||||
|
ProtoBackupImport.restore(
|
||||||
|
backup.content(),
|
||||||
|
BackupFlags.fromPartial(flags),
|
||||||
|
)
|
||||||
|
|
||||||
withTimeout(10.seconds) {
|
withTimeout(10.seconds) {
|
||||||
ProtoBackupImport.notifyFlow.first {
|
ProtoBackupImport.notifyFlow.first {
|
||||||
@@ -46,11 +53,18 @@ class BackupMutation {
|
|||||||
|
|
||||||
data class CreateBackupInput(
|
data class CreateBackupInput(
|
||||||
val clientMutationId: String? = null,
|
val clientMutationId: String? = null,
|
||||||
|
val flags: PartialBackupFlags? = null,
|
||||||
|
@GraphQLDeprecated("Will get removed", replaceWith = ReplaceWith("flags"))
|
||||||
val includeChapters: Boolean? = null,
|
val includeChapters: Boolean? = null,
|
||||||
|
@GraphQLDeprecated("Will get removed", replaceWith = ReplaceWith("flags"))
|
||||||
val includeCategories: Boolean? = null,
|
val includeCategories: Boolean? = null,
|
||||||
|
@GraphQLDeprecated("Will get removed", replaceWith = ReplaceWith("flags"))
|
||||||
val includeTracking: Boolean? = null,
|
val includeTracking: Boolean? = null,
|
||||||
|
@GraphQLDeprecated("Will get removed", replaceWith = ReplaceWith("flags"))
|
||||||
val includeHistory: Boolean? = null,
|
val includeHistory: Boolean? = null,
|
||||||
|
@GraphQLDeprecated("Will get removed", replaceWith = ReplaceWith("flags"))
|
||||||
val includeClientData: Boolean? = null,
|
val includeClientData: Boolean? = null,
|
||||||
|
@GraphQLDeprecated("Will get removed", replaceWith = ReplaceWith("flags"))
|
||||||
val includeServerSettings: Boolean? = null,
|
val includeServerSettings: Boolean? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -65,15 +79,19 @@ class BackupMutation {
|
|||||||
|
|
||||||
val backup =
|
val backup =
|
||||||
ProtoBackupExport.createBackup(
|
ProtoBackupExport.createBackup(
|
||||||
BackupFlags(
|
if (input?.flags != null) {
|
||||||
includeManga = true,
|
BackupFlags.fromPartial(input.flags)
|
||||||
includeCategories = input?.includeCategories ?: true,
|
} else {
|
||||||
includeChapters = input?.includeChapters ?: true,
|
BackupFlags(
|
||||||
includeTracking = input?.includeTracking ?: true,
|
includeManga = BackupFlags.DEFAULT.includeManga,
|
||||||
includeHistory = input?.includeHistory ?: true,
|
includeCategories = input?.includeCategories ?: BackupFlags.DEFAULT.includeCategories,
|
||||||
includeClientData = input?.includeClientData ?: true,
|
includeChapters = input?.includeChapters ?: BackupFlags.DEFAULT.includeChapters,
|
||||||
includeServerSettings = input?.includeServerSettings ?: true,
|
includeTracking = input?.includeTracking ?: BackupFlags.DEFAULT.includeTracking,
|
||||||
),
|
includeHistory = input?.includeHistory ?: BackupFlags.DEFAULT.includeHistory,
|
||||||
|
includeClientData = input?.includeClientData ?: BackupFlags.DEFAULT.includeClientData,
|
||||||
|
includeServerSettings = input?.includeServerSettings ?: BackupFlags.DEFAULT.includeServerSettings,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
TemporaryFileStorage.saveFile(filename, backup)
|
TemporaryFileStorage.saveFile(filename, backup)
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
package suwayomi.tachidesk.graphql.types
|
package suwayomi.tachidesk.graphql.types
|
||||||
|
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.IBackupFlags
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
|
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
|
||||||
|
|
||||||
|
data class PartialBackupFlags(
|
||||||
|
override val includeManga: Boolean?,
|
||||||
|
override val includeCategories: Boolean?,
|
||||||
|
override val includeChapters: Boolean?,
|
||||||
|
override val includeTracking: Boolean?,
|
||||||
|
override val includeHistory: Boolean?,
|
||||||
|
override val includeClientData: Boolean?,
|
||||||
|
override val includeServerSettings: Boolean?,
|
||||||
|
) : IBackupFlags
|
||||||
|
|
||||||
enum class BackupRestoreState {
|
enum class BackupRestoreState {
|
||||||
IDLE,
|
IDLE,
|
||||||
SUCCESS,
|
SUCCESS,
|
||||||
|
|||||||
@@ -89,17 +89,7 @@ object BackupController {
|
|||||||
ctx.contentType("application/octet-stream")
|
ctx.contentType("application/octet-stream")
|
||||||
ctx.future {
|
ctx.future {
|
||||||
future {
|
future {
|
||||||
ProtoBackupExport.createBackup(
|
ProtoBackupExport.createBackup(BackupFlags.DEFAULT)
|
||||||
BackupFlags(
|
|
||||||
includeManga = true,
|
|
||||||
includeCategories = true,
|
|
||||||
includeChapters = true,
|
|
||||||
includeTracking = true,
|
|
||||||
includeHistory = true,
|
|
||||||
includeClientData = true,
|
|
||||||
includeServerSettings = true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}.thenApply { ctx.result(it) }
|
}.thenApply { ctx.result(it) }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -124,17 +114,7 @@ object BackupController {
|
|||||||
ctx.header("Content-Disposition", """attachment; filename="${Backup.getFilename()}"""")
|
ctx.header("Content-Disposition", """attachment; filename="${Backup.getFilename()}"""")
|
||||||
ctx.future {
|
ctx.future {
|
||||||
future {
|
future {
|
||||||
ProtoBackupExport.createBackup(
|
ProtoBackupExport.createBackup(BackupFlags.DEFAULT)
|
||||||
BackupFlags(
|
|
||||||
includeManga = true,
|
|
||||||
includeCategories = true,
|
|
||||||
includeChapters = true,
|
|
||||||
includeTracking = true,
|
|
||||||
includeHistory = true,
|
|
||||||
includeClientData = true,
|
|
||||||
includeServerSettings = true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}.thenApply { ctx.result(it) }
|
}.thenApply { ctx.result(it) }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.backup
|
package suwayomi.tachidesk.manga.impl.backup
|
||||||
|
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
*
|
*
|
||||||
@@ -7,12 +9,46 @@ package suwayomi.tachidesk.manga.impl.backup
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* 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/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
interface IBackupFlags {
|
||||||
|
val includeManga: Boolean?
|
||||||
|
val includeCategories: Boolean?
|
||||||
|
val includeChapters: Boolean?
|
||||||
|
val includeTracking: Boolean?
|
||||||
|
val includeHistory: Boolean?
|
||||||
|
val includeClientData: Boolean?
|
||||||
|
val includeServerSettings: Boolean?
|
||||||
|
}
|
||||||
|
|
||||||
data class BackupFlags(
|
data class BackupFlags(
|
||||||
val includeManga: Boolean,
|
override val includeManga: Boolean,
|
||||||
val includeCategories: Boolean,
|
override val includeCategories: Boolean,
|
||||||
val includeChapters: Boolean,
|
override val includeChapters: Boolean,
|
||||||
val includeTracking: Boolean,
|
override val includeTracking: Boolean,
|
||||||
val includeHistory: Boolean,
|
override val includeHistory: Boolean,
|
||||||
val includeClientData: Boolean,
|
override val includeClientData: Boolean,
|
||||||
val includeServerSettings: Boolean,
|
override val includeServerSettings: Boolean,
|
||||||
)
|
) : IBackupFlags {
|
||||||
|
companion object {
|
||||||
|
val DEFAULT =
|
||||||
|
BackupFlags(
|
||||||
|
includeManga = true,
|
||||||
|
includeCategories = true,
|
||||||
|
includeChapters = true,
|
||||||
|
includeTracking = true,
|
||||||
|
includeHistory = true,
|
||||||
|
includeClientData = true,
|
||||||
|
includeServerSettings = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun fromPartial(partialFlags: IBackupFlags?): BackupFlags =
|
||||||
|
BackupFlags(
|
||||||
|
includeManga = partialFlags?.includeManga ?: DEFAULT.includeManga,
|
||||||
|
includeCategories = partialFlags?.includeCategories ?: DEFAULT.includeCategories,
|
||||||
|
includeChapters = partialFlags?.includeChapters ?: DEFAULT.includeChapters,
|
||||||
|
includeTracking = partialFlags?.includeTracking ?: DEFAULT.includeTracking,
|
||||||
|
includeHistory = partialFlags?.includeHistory ?: DEFAULT.includeHistory,
|
||||||
|
includeClientData = partialFlags?.includeClientData ?: DEFAULT.includeClientData,
|
||||||
|
includeServerSettings = partialFlags?.includeServerSettings ?: DEFAULT.includeServerSettings,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+10
-14
@@ -21,6 +21,7 @@ import okio.Sink
|
|||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import org.jetbrains.exposed.sql.Query
|
import org.jetbrains.exposed.sql.Query
|
||||||
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
@@ -118,17 +119,7 @@ object ProtoBackupExport : ProtoBackupBase() {
|
|||||||
private fun createAutomatedBackup() {
|
private fun createAutomatedBackup() {
|
||||||
logger.info { "Creating automated backup..." }
|
logger.info { "Creating automated backup..." }
|
||||||
|
|
||||||
createBackup(
|
createBackup(BackupFlags.DEFAULT).use { input ->
|
||||||
BackupFlags(
|
|
||||||
includeManga = true,
|
|
||||||
includeCategories = true,
|
|
||||||
includeChapters = true,
|
|
||||||
includeTracking = true,
|
|
||||||
includeHistory = true,
|
|
||||||
includeClientData = true,
|
|
||||||
includeServerSettings = true,
|
|
||||||
),
|
|
||||||
).use { input ->
|
|
||||||
val automatedBackupDir = File(applicationDirs.automatedBackupRoot)
|
val automatedBackupDir = File(applicationDirs.automatedBackupRoot)
|
||||||
automatedBackupDir.mkdirs()
|
automatedBackupDir.mkdirs()
|
||||||
|
|
||||||
@@ -179,7 +170,12 @@ object ProtoBackupExport : ProtoBackupBase() {
|
|||||||
fun createBackup(flags: BackupFlags): InputStream {
|
fun createBackup(flags: BackupFlags): InputStream {
|
||||||
// Create root object
|
// Create root object
|
||||||
|
|
||||||
val databaseManga = transaction { MangaTable.selectAll().where { MangaTable.inLibrary eq true } }
|
val databaseManga =
|
||||||
|
if (flags.includeManga) {
|
||||||
|
transaction { MangaTable.selectAll().where { MangaTable.inLibrary eq true }.toList() }
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
val backup: Backup =
|
val backup: Backup =
|
||||||
transaction {
|
transaction {
|
||||||
@@ -204,7 +200,7 @@ object ProtoBackupExport : ProtoBackupBase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun backupManga(
|
private fun backupManga(
|
||||||
databaseManga: Query,
|
databaseManga: List<ResultRow>,
|
||||||
flags: BackupFlags,
|
flags: BackupFlags,
|
||||||
): List<BackupManga> =
|
): List<BackupManga> =
|
||||||
databaseManga.map { mangaRow ->
|
databaseManga.map { mangaRow ->
|
||||||
@@ -336,7 +332,7 @@ object ProtoBackupExport : ProtoBackupBase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun backupExtensionInfo(
|
private fun backupExtensionInfo(
|
||||||
mangas: Query,
|
mangas: List<ResultRow>,
|
||||||
flags: BackupFlags,
|
flags: BackupFlags,
|
||||||
): List<BackupSource> {
|
): List<BackupSource> {
|
||||||
val inLibraryMangaSourceIds =
|
val inLibraryMangaSourceIds =
|
||||||
|
|||||||
+117
-76
@@ -39,6 +39,7 @@ import suwayomi.tachidesk.manga.impl.Chapter.modifyChaptersMetas
|
|||||||
import suwayomi.tachidesk.manga.impl.Manga.clearThumbnail
|
import suwayomi.tachidesk.manga.impl.Manga.clearThumbnail
|
||||||
import suwayomi.tachidesk.manga.impl.Manga.modifyMangasMetas
|
import suwayomi.tachidesk.manga.impl.Manga.modifyMangasMetas
|
||||||
import suwayomi.tachidesk.manga.impl.Source.modifySourceMetas
|
import suwayomi.tachidesk.manga.impl.Source.modifySourceMetas
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.ValidationResult
|
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.ValidationResult
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.validate
|
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.validate
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.handlers.BackupSettingsHandler
|
import suwayomi.tachidesk.manga.impl.backup.proto.handlers.BackupSettingsHandler
|
||||||
@@ -139,7 +140,10 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
fun restore(sourceStream: InputStream): String {
|
fun restore(
|
||||||
|
sourceStream: InputStream,
|
||||||
|
flags: BackupFlags,
|
||||||
|
): String {
|
||||||
val restoreId = System.currentTimeMillis().toString()
|
val restoreId = System.currentTimeMillis().toString()
|
||||||
|
|
||||||
logger.info { "restore($restoreId): queued" }
|
logger.info { "restore($restoreId): queued" }
|
||||||
@@ -147,7 +151,7 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
updateRestoreState(restoreId, BackupRestoreState.Idle)
|
updateRestoreState(restoreId, BackupRestoreState.Idle)
|
||||||
|
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
restoreLegacy(sourceStream, restoreId)
|
restoreLegacy(sourceStream, restoreId, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
return restoreId
|
return restoreId
|
||||||
@@ -156,11 +160,12 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
suspend fun restoreLegacy(
|
suspend fun restoreLegacy(
|
||||||
sourceStream: InputStream,
|
sourceStream: InputStream,
|
||||||
restoreId: String = "legacy",
|
restoreId: String = "legacy",
|
||||||
|
flags: BackupFlags = BackupFlags.DEFAULT,
|
||||||
): ValidationResult =
|
): ValidationResult =
|
||||||
backupMutex.withLock {
|
backupMutex.withLock {
|
||||||
try {
|
try {
|
||||||
logger.info { "restore($restoreId): restoring..." }
|
logger.info { "restore($restoreId): restoring..." }
|
||||||
performRestore(restoreId, sourceStream)
|
performRestore(restoreId, sourceStream, flags)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error(e) { "restore($restoreId): failed due to" }
|
logger.error(e) { "restore($restoreId): failed due to" }
|
||||||
|
|
||||||
@@ -180,6 +185,7 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
private fun performRestore(
|
private fun performRestore(
|
||||||
id: String,
|
id: String,
|
||||||
sourceStream: InputStream,
|
sourceStream: InputStream,
|
||||||
|
flags: BackupFlags,
|
||||||
): ValidationResult {
|
): ValidationResult {
|
||||||
val backupString =
|
val backupString =
|
||||||
sourceStream
|
sourceStream
|
||||||
@@ -191,28 +197,36 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
|
|
||||||
val validationResult = validate(backup)
|
val validationResult = validate(backup)
|
||||||
|
|
||||||
val restoreCategories = 1
|
val restoreCategories = if (flags.includeCategories) 1 else 0
|
||||||
val restoreMeta = 1
|
val restoreMeta = if (flags.includeClientData) 1 else 0
|
||||||
val restoreSettings = 1
|
val restoreSettings = if (flags.includeServerSettings) 1 else 0
|
||||||
val getRestoreAmount = { size: Int -> size + restoreCategories + restoreMeta + restoreSettings }
|
val getRestoreAmount = { size: Int -> size + restoreCategories + restoreMeta + restoreSettings }
|
||||||
val restoreAmount = getRestoreAmount(backup.backupManga.size)
|
val restoreAmount = getRestoreAmount(if (flags.includeManga) backup.backupManga.size else 0)
|
||||||
|
|
||||||
updateRestoreState(
|
if (flags.includeServerSettings) {
|
||||||
id,
|
updateRestoreState(
|
||||||
BackupRestoreState.RestoringSettings(restoreSettings, restoreAmount),
|
id,
|
||||||
)
|
BackupRestoreState.RestoringSettings(restoreSettings, restoreAmount),
|
||||||
|
)
|
||||||
|
|
||||||
BackupSettingsHandler.restore(backup.serverSettings)
|
BackupSettingsHandler.restore(backup.serverSettings)
|
||||||
|
}
|
||||||
|
|
||||||
updateRestoreState(id, BackupRestoreState.RestoringCategories(restoreSettings + restoreCategories, restoreAmount))
|
val categoryMapping =
|
||||||
|
if (flags.includeCategories) {
|
||||||
|
updateRestoreState(id, BackupRestoreState.RestoringCategories(restoreSettings + restoreCategories, restoreAmount))
|
||||||
|
restoreCategories(backup.backupCategories)
|
||||||
|
} else {
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
val categoryMapping = restoreCategories(backup.backupCategories)
|
if (flags.includeClientData) {
|
||||||
|
updateRestoreState(id, BackupRestoreState.RestoringMeta(restoreSettings + restoreCategories + restoreMeta, restoreAmount))
|
||||||
|
|
||||||
updateRestoreState(id, BackupRestoreState.RestoringMeta(restoreSettings + restoreCategories + restoreMeta, restoreAmount))
|
restoreGlobalMeta(backup.meta)
|
||||||
|
|
||||||
restoreGlobalMeta(backup.meta)
|
restoreSourceMeta(backup.backupSources)
|
||||||
|
}
|
||||||
restoreSourceMeta(backup.backupSources)
|
|
||||||
|
|
||||||
// Store source mapping for error messages
|
// Store source mapping for error messages
|
||||||
val sourceMapping = backup.getSourceMap()
|
val sourceMapping = backup.getSourceMap()
|
||||||
@@ -220,22 +234,25 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
val errors = mutableListOf<Pair<Date, String>>()
|
val errors = mutableListOf<Pair<Date, String>>()
|
||||||
|
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
backup.backupManga.forEachIndexed { index, manga ->
|
if (flags.includeManga) {
|
||||||
updateRestoreState(
|
backup.backupManga.forEachIndexed { index, manga ->
|
||||||
id,
|
updateRestoreState(
|
||||||
BackupRestoreState.RestoringManga(
|
id,
|
||||||
current = getRestoreAmount(index + 1),
|
BackupRestoreState.RestoringManga(
|
||||||
totalManga = restoreAmount,
|
current = getRestoreAmount(index + 1),
|
||||||
title = manga.title,
|
totalManga = restoreAmount,
|
||||||
),
|
title = manga.title,
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
|
||||||
restoreManga(
|
restoreManga(
|
||||||
backupManga = manga,
|
backupManga = manga,
|
||||||
categoryMapping = categoryMapping,
|
categoryMapping = categoryMapping,
|
||||||
sourceMapping = sourceMapping,
|
sourceMapping = sourceMapping,
|
||||||
errors = errors,
|
errors = errors,
|
||||||
)
|
flags = flags,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info {
|
logger.info {
|
||||||
@@ -279,15 +296,17 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
categoryMapping: Map<Int, Int>,
|
categoryMapping: Map<Int, Int>,
|
||||||
sourceMapping: Map<Long, String>,
|
sourceMapping: Map<Long, String>,
|
||||||
errors: MutableList<Pair<Date, String>>,
|
errors: MutableList<Pair<Date, String>>,
|
||||||
|
flags: BackupFlags,
|
||||||
) {
|
) {
|
||||||
val chapters = backupManga.chapters
|
val chapters = backupManga.chapters
|
||||||
val categories = backupManga.categories
|
val categories = backupManga.categories
|
||||||
val history = backupManga.history
|
val history = backupManga.history
|
||||||
|
val tracking = backupManga.tracking
|
||||||
|
|
||||||
val dbCategoryIds = categories.map { categoryMapping[it]!! }
|
val dbCategoryIds = categories.mapNotNull { categoryMapping[it] }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
restoreMangaData(backupManga, chapters, dbCategoryIds, history, backupManga.tracking)
|
restoreMangaData(backupManga, chapters, dbCategoryIds, history, tracking, flags)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val sourceName = sourceMapping[backupManga.source] ?: backupManga.source.toString()
|
val sourceName = sourceMapping[backupManga.source] ?: backupManga.source.toString()
|
||||||
errors.add(Date() to "${backupManga.title} [$sourceName]: ${e.message}")
|
errors.add(Date() to "${backupManga.title} [$sourceName]: ${e.message}")
|
||||||
@@ -300,6 +319,7 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
categoryIds: List<Int>,
|
categoryIds: List<Int>,
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<BackupTracking>,
|
tracks: List<BackupTracking>,
|
||||||
|
flags: BackupFlags,
|
||||||
) {
|
) {
|
||||||
val dbManga =
|
val dbManga =
|
||||||
transaction {
|
transaction {
|
||||||
@@ -362,20 +382,26 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
// delete thumbnail in case cached data still exists
|
// delete thumbnail in case cached data still exists
|
||||||
clearThumbnail(mangaId)
|
clearThumbnail(mangaId)
|
||||||
|
|
||||||
if (manga.meta.isNotEmpty()) {
|
if (flags.includeClientData && manga.meta.isNotEmpty()) {
|
||||||
modifyMangasMetas(mapOf(mangaId to manga.meta))
|
modifyMangasMetas(mapOf(mangaId to manga.meta))
|
||||||
}
|
}
|
||||||
|
|
||||||
// merge chapter data
|
// merge chapter data
|
||||||
restoreMangaChapterData(mangaId, restoreMode, chapters, history)
|
if (flags.includeChapters || flags.includeHistory) {
|
||||||
|
restoreMangaChapterData(mangaId, restoreMode, chapters, history, flags)
|
||||||
|
}
|
||||||
|
|
||||||
// merge categories
|
// merge categories
|
||||||
restoreMangaCategoryData(mangaId, categoryIds)
|
if (flags.includeCategories) {
|
||||||
|
restoreMangaCategoryData(mangaId, categoryIds)
|
||||||
|
}
|
||||||
|
|
||||||
mangaId
|
mangaId
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreMangaTrackerData(mangaId, tracks)
|
if (flags.includeTracking) {
|
||||||
|
restoreMangaTrackerData(mangaId, tracks)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: insert/merge history
|
// TODO: insert/merge history
|
||||||
}
|
}
|
||||||
@@ -404,64 +430,79 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
restoreMode: RestoreMode,
|
restoreMode: RestoreMode,
|
||||||
chapters: List<BackupChapter>,
|
chapters: List<BackupChapter>,
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
|
flags: BackupFlags,
|
||||||
) = dbTransaction {
|
) = dbTransaction {
|
||||||
val (chaptersToInsert, chaptersToUpdateToDbChapter) = getMangaChapterToRestoreInfo(mangaId, restoreMode, chapters)
|
val (chaptersToInsert, chaptersToUpdateToDbChapter) = getMangaChapterToRestoreInfo(mangaId, restoreMode, chapters)
|
||||||
val historyByChapter = history.groupBy({ it.url }, { it.lastRead })
|
val historyByChapter = history.groupBy({ it.url }, { it.lastRead })
|
||||||
|
|
||||||
val insertedChapterIds =
|
val insertedChapterIds =
|
||||||
ChapterTable
|
if (flags.includeChapters) {
|
||||||
.batchInsert(chaptersToInsert) { chapter ->
|
ChapterTable
|
||||||
this[ChapterTable.url] = chapter.url
|
.batchInsert(chaptersToInsert) { chapter ->
|
||||||
this[ChapterTable.name] = chapter.name
|
this[ChapterTable.url] = chapter.url
|
||||||
if (chapter.dateUpload == 0L) {
|
this[ChapterTable.name] = chapter.name
|
||||||
this[ChapterTable.date_upload] = chapter.dateFetch
|
if (chapter.dateUpload == 0L) {
|
||||||
} else {
|
this[ChapterTable.date_upload] = chapter.dateFetch
|
||||||
this[ChapterTable.date_upload] = chapter.dateUpload
|
} else {
|
||||||
}
|
this[ChapterTable.date_upload] = chapter.dateUpload
|
||||||
this[ChapterTable.chapter_number] = chapter.chapterNumber
|
}
|
||||||
this[ChapterTable.scanlator] = chapter.scanlator
|
this[ChapterTable.chapter_number] = chapter.chapterNumber
|
||||||
|
this[ChapterTable.scanlator] = chapter.scanlator
|
||||||
|
|
||||||
this[ChapterTable.sourceOrder] = chaptersToInsert.size - chapter.sourceOrder
|
this[ChapterTable.sourceOrder] = chaptersToInsert.size - chapter.sourceOrder
|
||||||
this[ChapterTable.manga] = mangaId
|
this[ChapterTable.manga] = mangaId
|
||||||
|
|
||||||
this[ChapterTable.isRead] = chapter.read
|
this[ChapterTable.isRead] = chapter.read
|
||||||
this[ChapterTable.lastPageRead] = chapter.lastPageRead.coerceAtLeast(0)
|
this[ChapterTable.lastPageRead] = chapter.lastPageRead.coerceAtLeast(0)
|
||||||
this[ChapterTable.isBookmarked] = chapter.bookmark
|
this[ChapterTable.isBookmarked] = chapter.bookmark
|
||||||
|
|
||||||
this[ChapterTable.fetchedAt] = chapter.dateFetch.milliseconds.inWholeSeconds
|
this[ChapterTable.fetchedAt] = chapter.dateFetch.milliseconds.inWholeSeconds
|
||||||
|
|
||||||
this[ChapterTable.lastReadAt] = historyByChapter[chapter.url]?.maxOrNull()?.milliseconds?.inWholeSeconds ?: 0
|
if (flags.includeHistory) {
|
||||||
}.map { it[ChapterTable.id].value }
|
this[ChapterTable.lastReadAt] =
|
||||||
|
historyByChapter[chapter.url]?.maxOrNull()?.milliseconds?.inWholeSeconds ?: 0
|
||||||
|
}
|
||||||
|
}.map { it[ChapterTable.id].value }
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
if (chaptersToUpdateToDbChapter.isNotEmpty()) {
|
if (chaptersToUpdateToDbChapter.isNotEmpty()) {
|
||||||
BatchUpdateStatement(ChapterTable).apply {
|
BatchUpdateStatement(ChapterTable).apply {
|
||||||
chaptersToUpdateToDbChapter.forEach { (backupChapter, dbChapter) ->
|
chaptersToUpdateToDbChapter.forEach { (backupChapter, dbChapter) ->
|
||||||
addBatch(EntityID(dbChapter[ChapterTable.id].value, ChapterTable))
|
addBatch(EntityID(dbChapter[ChapterTable.id].value, ChapterTable))
|
||||||
this[ChapterTable.isRead] = backupChapter.read || dbChapter[ChapterTable.isRead]
|
if (flags.includeChapters) {
|
||||||
this[ChapterTable.lastPageRead] =
|
this[ChapterTable.isRead] = backupChapter.read || dbChapter[ChapterTable.isRead]
|
||||||
max(backupChapter.lastPageRead, dbChapter[ChapterTable.lastPageRead]).coerceAtLeast(0)
|
this[ChapterTable.lastPageRead] =
|
||||||
this[ChapterTable.isBookmarked] = backupChapter.bookmark || dbChapter[ChapterTable.isBookmarked]
|
max(backupChapter.lastPageRead, dbChapter[ChapterTable.lastPageRead]).coerceAtLeast(0)
|
||||||
this[ChapterTable.lastReadAt] =
|
this[ChapterTable.isBookmarked] = backupChapter.bookmark || dbChapter[ChapterTable.isBookmarked]
|
||||||
(historyByChapter[backupChapter.url]?.maxOrNull()?.milliseconds?.inWholeSeconds ?: 0)
|
}
|
||||||
.coerceAtLeast(dbChapter[ChapterTable.lastReadAt])
|
|
||||||
|
if (flags.includeHistory) {
|
||||||
|
this[ChapterTable.lastReadAt] =
|
||||||
|
(historyByChapter[backupChapter.url]?.maxOrNull()?.milliseconds?.inWholeSeconds ?: 0)
|
||||||
|
.coerceAtLeast(dbChapter[ChapterTable.lastReadAt])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
execute(this@dbTransaction)
|
execute(this@dbTransaction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val chaptersToInsertByChapterId = insertedChapterIds.zip(chaptersToInsert)
|
if (flags.includeClientData) {
|
||||||
val chapterToUpdateByChapterId =
|
val chaptersToInsertByChapterId = insertedChapterIds.zip(chaptersToInsert)
|
||||||
chaptersToUpdateToDbChapter.map { (backupChapter, dbChapter) ->
|
val chapterToUpdateByChapterId =
|
||||||
dbChapter[ChapterTable.id].value to
|
chaptersToUpdateToDbChapter.map { (backupChapter, dbChapter) ->
|
||||||
backupChapter
|
dbChapter[ChapterTable.id].value to
|
||||||
}
|
backupChapter
|
||||||
val metaEntryByChapterId =
|
|
||||||
(chaptersToInsertByChapterId + chapterToUpdateByChapterId)
|
|
||||||
.associate { (chapterId, backupChapter) ->
|
|
||||||
chapterId to backupChapter.meta
|
|
||||||
}
|
}
|
||||||
|
val metaEntryByChapterId =
|
||||||
|
(chaptersToInsertByChapterId + chapterToUpdateByChapterId)
|
||||||
|
.associate { (chapterId, backupChapter) ->
|
||||||
|
chapterId to backupChapter.meta
|
||||||
|
}
|
||||||
|
|
||||||
modifyChaptersMetas(metaEntryByChapterId)
|
modifyChaptersMetas(metaEntryByChapterId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreMangaCategoryData(
|
private fun restoreMangaCategoryData(
|
||||||
|
|||||||
-4
@@ -28,10 +28,6 @@ object ProtoBackupValidator {
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun validate(backup: Backup): ValidationResult {
|
fun validate(backup: Backup): ValidationResult {
|
||||||
if (backup.backupManga.isEmpty()) {
|
|
||||||
throw Exception("Backup does not contain any manga.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val sources = backup.getSourceMap()
|
val sources = backup.getSourceMap()
|
||||||
|
|
||||||
val missingSources =
|
val missingSources =
|
||||||
|
|||||||
Reference in New Issue
Block a user