Warn about missing sources before restoring backup

This commit is contained in:
arkon 2020-05-16 11:18:47 -04:00
parent 1cf74a5396
commit a00d11701f
5 changed files with 108 additions and 14 deletions

View file

@ -110,6 +110,11 @@ class BackupRestoreService : Service() {
*/ */
private var restoreAmount = 0 private var restoreAmount = 0
/**
* Mapping of source ID to source name from backup data
*/
private var sourceMapping: Map<Long, String> = emptyMap()
/** /**
* List containing errors * List containing errors
*/ */
@ -212,6 +217,9 @@ class BackupRestoreService : Service() {
// Restore categories // Restore categories
restoreCategories(json.get(CATEGORIES)) restoreCategories(json.get(CATEGORIES))
// Store source mapping for error messages
sourceMapping = BackupRestoreValidator.getSourceMapping(json)
// Restore individual manga // Restore individual manga
mangasJson.forEach { mangasJson.forEach {
if (job?.isActive != true) { if (job?.isActive != true) {
@ -259,9 +267,20 @@ class BackupRestoreService : Service() {
) )
try { try {
restoreMangaData(manga, chapters, categories, history, tracks) val source = backupManager.sourceManager.get(manga.source)
if (source != null) {
restoreMangaData(manga, source, chapters, categories, history, tracks)
} else {
val message = if (manga.source in sourceMapping) {
getString(R.string.source_not_found_name, sourceMapping[manga.source])
} else {
getString(R.string.source_not_found)
}
errors.add(Date() to "${manga.title} - $message")
}
} catch (e: Exception) { } catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}") errors.add(Date() to "${manga.title} - ${e.message}")
} }
restoreProgress += 1 restoreProgress += 1
@ -272,6 +291,7 @@ class BackupRestoreService : Service() {
* Returns a manga restore observable * Returns a manga restore observable
* *
* @param manga manga data from json * @param manga manga data from json
* @param source source to get manga data from
* @param chapters chapters data from json * @param chapters chapters data from json
* @param categories categories data from json * @param categories categories data from json
* @param history history data from json * @param history history data from json
@ -279,13 +299,12 @@ class BackupRestoreService : Service() {
*/ */
private fun restoreMangaData( private fun restoreMangaData(
manga: Manga, manga: Manga,
source: Source,
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<String>, categories: List<String>,
history: List<DHistory>, history: List<DHistory>,
tracks: List<Track> tracks: List<Track>
) { ) {
// Get source
val source = backupManager.sourceManager.getOrStub(manga.source)
val dbManga = backupManager.getMangaFromDatabase(manga) val dbManga = backupManager.getMangaFromDatabase(manga)
db.inTransaction { db.inTransaction {

View file

@ -0,0 +1,46 @@
package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.models.Backup
object BackupRestoreValidator {
/**
* Checks for critical backup file data.
*
* @throws Exception if version or manga cannot be found.
* @return List of required sources.
*/
fun validate(context: Context, uri: Uri): Map<Long, String> {
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
val json = JsonParser.parseReader(reader).asJsonObject
val version = json.get(Backup.VERSION)
val mangasJson = json.get(Backup.MANGAS)
if (version == null || mangasJson == null) {
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
}
if (mangasJson.asJsonArray.size() == 0) {
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
}
return getSourceMapping(json)
}
fun getSourceMapping(json: JsonObject): Map<Long, String> {
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
return extensionsMapping.asJsonArray
.map {
val items = it.asString.split(":")
items[0].toLong() to items[1]
}
.toMap()
}
}

View file

@ -16,9 +16,11 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupCreateService import eu.kanade.tachiyomi.data.backup.BackupCreateService
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.backup.BackupRestoreService import eu.kanade.tachiyomi.data.backup.BackupRestoreService
import eu.kanade.tachiyomi.data.backup.BackupRestoreValidator
import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.asImmediateFlow import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.util.preference.defaultValue import eu.kanade.tachiyomi.util.preference.defaultValue
@ -34,6 +36,8 @@ import eu.kanade.tachiyomi.util.system.getFilePicker
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SettingsBackupController : SettingsController() { class SettingsBackupController : SettingsController() {
@ -247,15 +251,36 @@ class SettingsBackupController : SettingsController() {
) )
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog(activity!!) val activity = activity!!
.title(R.string.pref_restore_backup) val uri: Uri = args.getParcelable(KEY_URI)!!
.message(R.string.backup_restore_content)
.positiveButton(R.string.action_restore) { return try {
val context = applicationContext var message = activity.getString(R.string.backup_restore_content)
if (context != null) {
BackupRestoreService.start(context, args.getParcelable(KEY_URI)!!) val sources = BackupRestoreValidator.validate(activity, uri)
if (sources.isNotEmpty()) {
val sourceManager = Injekt.get<SourceManager>()
val missingSources = sources
.filter { sourceManager.get(it.key) == null }
.values
.sorted()
if (missingSources.isNotEmpty()) {
message += "\n\n${activity.getString(R.string.backup_restore_missing_sources)}\n${missingSources.joinToString("\n") { "- $it" }}"
} }
} }
MaterialDialog(activity)
.title(R.string.pref_restore_backup)
.message(text = message)
.positiveButton(R.string.action_restore) {
BackupRestoreService.start(activity, uri)
}
} catch (e: Exception) {
MaterialDialog(activity)
.title(R.string.invalid_backup_file)
.message(text = e.message)
.positiveButton(android.R.string.cancel)
}
} }
private companion object { private companion object {

View file

@ -96,7 +96,7 @@
android:background="@drawable/list_item_selector" android:background="@drawable/list_item_selector"
android:gravity="center_vertical" android:gravity="center_vertical"
android:padding="16dp" android:padding="16dp"
android:text="@string/ext_preferences" android:text="@string/label_settings"
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View file

@ -209,7 +209,6 @@
<string name="ext_unofficial">Unofficial</string> <string name="ext_unofficial">Unofficial</string>
<string name="ext_untrusted">Untrusted</string> <string name="ext_untrusted">Untrusted</string>
<string name="ext_uninstall">Uninstall</string> <string name="ext_uninstall">Uninstall</string>
<string name="ext_preferences">Preferences</string>
<string name="ext_available">Available</string> <string name="ext_available">Available</string>
<string name="untrusted_extension">Untrusted extension</string> <string name="untrusted_extension">Untrusted extension</string>
<string name="untrusted_extension_message">This extension was signed with an untrusted certificate and wasn\'t activated.\n\nA malicious extension could read any login credentials stored in Tachiyomi or execute arbitrary code.\n\nBy trusting this certificate you accept these risks.</string> <string name="untrusted_extension_message">This extension was signed with an untrusted certificate and wasn\'t activated.\n\nA malicious extension could read any login credentials stored in Tachiyomi or execute arbitrary code.\n\nBy trusting this certificate you accept these risks.</string>
@ -327,14 +326,19 @@
<string name="pref_backup_interval">Backup frequency</string> <string name="pref_backup_interval">Backup frequency</string>
<string name="pref_backup_slots">Maximum backups</string> <string name="pref_backup_slots">Maximum backups</string>
<string name="source_not_found">Source not found</string> <string name="source_not_found">Source not found</string>
<string name="source_not_found_name">Source not found: %1$s</string>
<string name="backup_created">Backup created</string> <string name="backup_created">Backup created</string>
<string name="invalid_backup_file">Invalid backup file</string>
<string name="invalid_backup_file_missing_data">File is missing data.</string>
<string name="invalid_backup_file_missing_manga">Backup does not contain any manga.</string>
<string name="backup_restore_missing_sources">Missing sources:</string>
<string name="backup_restore_content">Restore uses sources to fetch data, carrier costs may apply.\n\nMake sure you have installed all necessary extensions and are logged in to sources and tracking services before restoring.</string>
<string name="restore_completed">Restore completed</string> <string name="restore_completed">Restore completed</string>
<string name="restore_duration">%02d min, %02d sec</string> <string name="restore_duration">%02d min, %02d sec</string>
<plurals name="restore_completed_message"> <plurals name="restore_completed_message">
<item quantity="one">Done in %1$s with %2$s error</item> <item quantity="one">Done in %1$s with %2$s error</item>
<item quantity="other">Done in %1$s with %2$s errors</item> <item quantity="other">Done in %1$s with %2$s errors</item>
</plurals> </plurals>
<string name="backup_restore_content">Restore uses sources to fetch data, carrier costs may apply.\n\nMake sure you have installed all necessary extensions and are logged in to sources and tracking services before restoring.</string>
<string name="backup_in_progress">Backup is already in progress</string> <string name="backup_in_progress">Backup is already in progress</string>
<string name="backup_choice">What do you want to backup?</string> <string name="backup_choice">What do you want to backup?</string>
<string name="creating_backup">Creating backup</string> <string name="creating_backup">Creating backup</string>