Warn about missing sources before restoring backup
This commit is contained in:
parent
1cf74a5396
commit
a00d11701f
5 changed files with 108 additions and 14 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Reference in a new issue