diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt index e2f15efce..6e4cc9236 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt +++ b/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 = 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, categories: List, history: List, tracks: List ) { - // Get source - val source = backupManager.sourceManager.getOrStub(manga.source) val dbManga = backupManager.getMangaFromDatabase(manga) db.inTransaction { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreValidator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreValidator.kt new file mode 100644 index 000000000..f5f924ac9 --- /dev/null +++ b/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 { + 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 { + val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap() + + return extensionsMapping.asJsonArray + .map { + val items = it.asString.split(":") + items[0].toLong() to items[1] + } + .toMap() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt index 87c5f9d66..cc8183ab9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt +++ b/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() + 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 { diff --git a/app/src/main/res/layout/extension_detail_controller.xml b/app/src/main/res/layout/extension_detail_controller.xml index 88350a246..da0689e46 100644 --- a/app/src/main/res/layout/extension_detail_controller.xml +++ b/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" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 788a8bcec..3e1b9cbbd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -209,7 +209,6 @@ Unofficial Untrusted Uninstall - Preferences Available Untrusted extension 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. @@ -327,14 +326,19 @@ Backup frequency Maximum backups Source not found + Source not found: %1$s Backup created + Invalid backup file + File is missing data. + Backup does not contain any manga. + Missing sources: + 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. Restore completed %02d min, %02d sec Done in %1$s with %2$s error Done in %1$s with %2$s errors - 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. Backup is already in progress What do you want to backup? Creating backup