Backup creation and restore gql endpoints (#587)

This commit is contained in:
Mitchell Syer
2023-07-02 11:45:44 -04:00
committed by GitHub
parent 1a9a0b3394
commit a11b654c3d
13 changed files with 477 additions and 43 deletions

View File

@@ -35,7 +35,7 @@ import java.io.InputStream
import java.util.concurrent.TimeUnit
object ProtoBackupExport : ProtoBackupBase() {
suspend fun createBackup(flags: BackupFlags): InputStream {
fun createBackup(flags: BackupFlags): InputStream {
// Create root object
val databaseManga = transaction { MangaTable.select { MangaTable.inLibrary eq true } }

View File

@@ -7,6 +7,9 @@ package suwayomi.tachidesk.manga.impl.backup.proto
* 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 kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import mu.KotlinLogging
import okio.buffer
import okio.gzip
@@ -43,35 +46,55 @@ object ProtoBackupImport : ProtoBackupBase() {
private val errors = mutableListOf<Pair<Date, String>>()
private val backupMutex = Mutex()
sealed class BackupRestoreState {
object Idle : BackupRestoreState()
data class RestoringCategories(val totalManga: Int) : BackupRestoreState()
data class RestoringManga(val current: Int, val totalManga: Int, val title: String) : BackupRestoreState()
}
val backupRestoreState = MutableStateFlow<BackupRestoreState>(BackupRestoreState.Idle)
suspend fun performRestore(sourceStream: InputStream): ValidationResult {
val backupString = sourceStream.source().gzip().buffer().use { it.readByteArray() }
val backup = parser.decodeFromByteArray(BackupSerializer, backupString)
return backupMutex.withLock {
val backupString = sourceStream.source().gzip().buffer().use { it.readByteArray() }
val backup = parser.decodeFromByteArray(BackupSerializer, backupString)
val validationResult = validate(backup)
val validationResult = validate(backup)
restoreAmount = backup.backupManga.size + 1 // +1 for categories
restoreAmount = backup.backupManga.size + 1 // +1 for categories
// Restore categories
if (backup.backupCategories.isNotEmpty()) {
restoreCategories(backup.backupCategories)
}
val categoryMapping = transaction {
backup.backupCategories.associate {
it.order to CategoryTable.select { CategoryTable.name eq it.name }.first()[CategoryTable.id].value
backupRestoreState.value = BackupRestoreState.RestoringCategories(backup.backupManga.size)
// Restore categories
if (backup.backupCategories.isNotEmpty()) {
restoreCategories(backup.backupCategories)
}
}
// Store source mapping for error messages
sourceMapping = backup.getSourceMap()
val categoryMapping = transaction {
backup.backupCategories.associate {
it.order to CategoryTable.select { CategoryTable.name eq it.name }.first()[CategoryTable.id].value
}
}
// Restore individual manga
backup.backupManga.forEach {
restoreManga(it, backup.backupCategories, categoryMapping)
}
// Store source mapping for error messages
sourceMapping = backup.getSourceMap()
logger.info {
"""
// Restore individual manga
backup.backupManga.forEachIndexed { index, manga ->
backupRestoreState.value = BackupRestoreState.RestoringManga(
current = index + 1,
totalManga = backup.backupManga.size,
title = manga.title
)
restoreManga(
backupManga = manga,
backupCategories = backup.backupCategories,
categoryMapping = categoryMapping
)
}
logger.info {
"""
Restore Errors:
${errors.joinToString("\n") { "${it.first} - ${it.second}" }}
Restore Summary:
@@ -81,10 +104,12 @@ object ProtoBackupImport : ProtoBackupBase() {
${validationResult.mangasMissingSources.joinToString("\n ")}
- Missing Trackers:
${validationResult.missingTrackers.joinToString("\n ")}
""".trimIndent()
}
""".trimIndent()
}
backupRestoreState.value = BackupRestoreState.Idle
return validationResult
validationResult
}
}
private fun restoreCategories(backupCategories: List<BackupCategory>) {

View File

@@ -7,6 +7,7 @@ package suwayomi.tachidesk.manga.impl.backup.proto
* 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 com.fasterxml.jackson.annotation.JsonIgnore
import okio.buffer
import okio.gzip
import okio.source
@@ -21,7 +22,9 @@ object ProtoBackupValidator {
data class ValidationResult(
val missingSources: List<String>,
val missingTrackers: List<String>,
val mangasMissingSources: List<String>
val mangasMissingSources: List<String>,
@JsonIgnore
val missingSourceIds: List<Pair<Long, String>>
)
fun validate(backup: Backup): ValidationResult {
@@ -32,18 +35,9 @@ object ProtoBackupValidator {
val sources = backup.getSourceMap()
val missingSources = transaction {
sources
.filter { SourceTable.select { SourceTable.id eq it.key }.firstOrNull() == null }
.map { "${it.value} (${it.key})" }
.sorted()
sources.filter { SourceTable.select { SourceTable.id eq it.key }.firstOrNull() == null }
}
val brokenSourceIds = backup.brokenBackupSources.map { it.sourceId }
val mangasMissingSources = backup.backupManga
.filter { it.source in brokenSourceIds }
.map { manga -> "${manga.title} (from ${backup.brokenBackupSources.first { it.sourceId == manga.source }.name})" }
// val trackers = backup.backupManga
// .flatMap { it.tracking }
// .map { it.syncId }
@@ -56,10 +50,17 @@ object ProtoBackupValidator {
// .map { context.getString(it.nameRes()) }
// .sorted()
return ValidationResult(missingSources, missingTrackers, mangasMissingSources)
return ValidationResult(
missingSources
.map { "${it.value} (${it.key})" }
.sorted(),
missingTrackers,
emptyList(),
missingSources.toList()
)
}
suspend fun validate(sourceStream: InputStream): ValidationResult {
fun validate(sourceStream: InputStream): ValidationResult {
val backupString = sourceStream.source().gzip().buffer().use { it.readByteArray() }
val backup = ProtoBackupImport.parser.decodeFromByteArray(BackupSerializer, backupString)