Browse Source

Warn about missing sources before restoring backup

arkon 4 years ago
parent
commit
a00d11701f

+ 23 - 4
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt

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

+ 46 - 0
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreValidator.kt

@@ -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()
+    }
+}

+ 32 - 7
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt

@@ -16,9 +16,11 @@ import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.backup.BackupCreateService
 import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
 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.preference.PreferenceKeys as Keys
 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.requestPermissionsSafe
 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 kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
 
 class SettingsBackupController : SettingsController() {
 
@@ -247,15 +251,36 @@ class SettingsBackupController : SettingsController() {
         )
 
         override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-            return MaterialDialog(activity!!)
-                .title(R.string.pref_restore_backup)
-                .message(R.string.backup_restore_content)
-                .positiveButton(R.string.action_restore) {
-                    val context = applicationContext
-                    if (context != null) {
-                        BackupRestoreService.start(context, args.getParcelable(KEY_URI)!!)
+            val activity = activity!!
+            val uri: Uri = args.getParcelable(KEY_URI)!!
+
+            return try {
+                var message = activity.getString(R.string.backup_restore_content)
+
+                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 {

+ 1 - 1
app/src/main/res/layout/extension_detail_controller.xml

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

+ 6 - 2
app/src/main/res/values/strings.xml

@@ -209,7 +209,6 @@
     <string name="ext_unofficial">Unofficial</string>
     <string name="ext_untrusted">Untrusted</string>
     <string name="ext_uninstall">Uninstall</string>
-    <string name="ext_preferences">Preferences</string>
     <string name="ext_available">Available</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>
@@ -327,14 +326,19 @@
     <string name="pref_backup_interval">Backup frequency</string>
     <string name="pref_backup_slots">Maximum backups</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="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_duration">%02d min, %02d sec</string>
     <plurals name="restore_completed_message">
         <item quantity="one">Done in %1$s with %2$s error</item>
         <item quantity="other">Done in %1$s with %2$s errors</item>
     </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_choice">What do you want to backup?</string>
     <string name="creating_backup">Creating backup</string>