Fix backup restore dropping library entries (#3667)
* Fix backup restore dropping library entries History restore resolved chapters by URL alone, so a backup holding the same entry twice under one source matched multiple chapter rows and threw `ResultSet returned more than 1 row`. Scope the lookups to the entry. Entries are restored in chunks of 100 sharing one transaction. SQLDelight fails the enclosing transaction when a nested one fails, so catching the per-entry exception did not contain it and the whole chunk rolled back, losing up to 100 entries per bad one. Retry entry by entry on failure. Refs #647 * Update changelog --------- Co-authored-by: Naji Astier <na-ji@users.noreply.github.com>
This commit is contained in:
@@ -148,13 +148,30 @@ class BackupRestorer(
|
||||
mangaRestorer.sortByNew(backupMangas)
|
||||
.chunked(100)
|
||||
.forEach { chunk ->
|
||||
database.transaction {
|
||||
val restoredAsBatch = try {
|
||||
database.transaction {
|
||||
chunk.forEach {
|
||||
ensureActive()
|
||||
mangaRestorer.restore(it, backupCategories)
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
ensureActive()
|
||||
logcat(LogPriority.WARN, e) { "Batch restore failed, retrying entry by entry" }
|
||||
false
|
||||
}
|
||||
|
||||
if (restoredAsBatch) {
|
||||
restoreProgress.addAndFetch(chunk.size)
|
||||
} else {
|
||||
chunk.forEach {
|
||||
ensureActive()
|
||||
|
||||
try {
|
||||
mangaRestorer.restore(it, backupCategories)
|
||||
} catch (e: Exception) {
|
||||
ensureActive()
|
||||
val sourceName = sourceMapping[it.source] ?: it.source.toString()
|
||||
errors.add(Date() to "${it.title} [$sourceName]: ${e.message}")
|
||||
}
|
||||
@@ -162,6 +179,7 @@ class BackupRestorer(
|
||||
restoreProgress.incrementAndFetch()
|
||||
}
|
||||
}
|
||||
|
||||
notifier.showRestoreProgress(chunk.last().title, restoreProgress.load(), restoreAmount, isSync)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -284,7 +284,7 @@ class MangaRestorer(
|
||||
restoreCategories(manga, categories, backupCategories)
|
||||
restoreChapters(manga, chapters)
|
||||
restoreTracking(manga, tracks)
|
||||
restoreHistory(history)
|
||||
restoreHistory(manga, history)
|
||||
restoreExcludedScanlators(manga, excludedScanlators)
|
||||
updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow)
|
||||
return manga
|
||||
@@ -324,16 +324,16 @@ class MangaRestorer(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreHistory(backupHistory: List<BackupHistory>) {
|
||||
private suspend fun restoreHistory(manga: Manga, backupHistory: List<BackupHistory>) {
|
||||
val toUpdate = backupHistory.mapNotNull { history ->
|
||||
val dbHistory = database.historyQueries
|
||||
.getHistoryByChapterUrl(history.url)
|
||||
.getHistoryByChapterUrlAndMangaId(history.url, manga.id)
|
||||
.awaitAsOneOrNull()
|
||||
val item = history.getHistoryImpl()
|
||||
|
||||
if (dbHistory == null) {
|
||||
val chapter = database.chaptersQueries
|
||||
.getChapterByUrl(history.url)
|
||||
.getChapterByUrlAndMangaId(history.url, manga.id)
|
||||
.awaitAsOneOrNull()
|
||||
return@mapNotNull if (chapter == null) {
|
||||
// Chapter doesn't exist; skip
|
||||
|
||||
Reference in New Issue
Block a user