Foundations for partial restores

Related to #3136
This commit is contained in:
arkon 2023-12-21 22:16:42 -05:00
parent a51108cbe8
commit 83a67feb48
3 changed files with 78 additions and 14 deletions

View file

@ -40,6 +40,7 @@ import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.backup.BackupFileValidator import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
@ -249,7 +250,16 @@ object SettingsDataScreen : SearchableSettings {
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
BackupRestoreJob.start(context, err.uri) BackupRestoreJob.start(
context = context,
uri = err.uri,
// TODO: allow user-selectable restore options
options = RestoreOptions(
appSettings = true,
sourceSettings = true,
library = true,
),
)
onDismissRequest() onDismissRequest()
}, },
) { ) {
@ -283,7 +293,16 @@ object SettingsDataScreen : SearchableSettings {
} }
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) { if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
BackupRestoreJob.start(context, it) BackupRestoreJob.start(
context = context,
uri = it,
// TODO: allow user-selectable restore options
options = RestoreOptions(
appSettings = true,
sourceSettings = true,
library = true,
),
)
return@rememberLauncherForActivityResult return@rememberLauncherForActivityResult
} }

View file

@ -30,13 +30,19 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
?: return Result.failure() val options = inputData.getBooleanArray(OPTIONS_KEY)
?.let { RestoreOptions.fromBooleanArray(it) }
if (uri == null || options == null) {
return Result.failure()
}
val isSync = inputData.getBoolean(SYNC_KEY, false) val isSync = inputData.getBoolean(SYNC_KEY, false)
setForegroundSafely() setForegroundSafely()
return try { return try {
BackupRestorer(context, notifier, isSync).restore(uri) BackupRestorer(context, notifier, isSync).restore(uri, options)
Result.success() Result.success()
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) { if (e is CancellationException) {
@ -69,10 +75,16 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
return context.workManager.isRunning(TAG) return context.workManager.isRunning(TAG)
} }
fun start(context: Context, uri: Uri, sync: Boolean = false) { fun start(
context: Context,
uri: Uri,
options: RestoreOptions,
sync: Boolean = false,
) {
val inputData = workDataOf( val inputData = workDataOf(
LOCATION_URI_KEY to uri.toString(), LOCATION_URI_KEY to uri.toString(),
SYNC_KEY to sync, SYNC_KEY to sync,
OPTIONS_KEY to options.toBooleanArray(),
) )
val request = OneTimeWorkRequestBuilder<BackupRestoreJob>() val request = OneTimeWorkRequestBuilder<BackupRestoreJob>()
.addTag(TAG) .addTag(TAG)
@ -91,3 +103,4 @@ private const val TAG = "BackupRestore"
private const val LOCATION_URI_KEY = "location_uri" // String private const val LOCATION_URI_KEY = "location_uri" // String
private const val SYNC_KEY = "sync" // Boolean private const val SYNC_KEY = "sync" // Boolean
private const val OPTIONS_KEY = "options" // BooleanArray

View file

@ -39,10 +39,10 @@ class BackupRestorer(
*/ */
private var sourceMapping: Map<Long, String> = emptyMap() private var sourceMapping: Map<Long, String> = emptyMap()
suspend fun restore(uri: Uri) { suspend fun restore(uri: Uri, options: RestoreOptions) {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
restoreFromFile(uri) restoreFromFile(uri, options)
val time = System.currentTimeMillis() - startTime val time = System.currentTimeMillis() - startTime
@ -57,20 +57,36 @@ class BackupRestorer(
) )
} }
private suspend fun restoreFromFile(uri: Uri) { private suspend fun restoreFromFile(uri: Uri, options: RestoreOptions) {
val backup = BackupUtil.decodeBackup(context, uri) val backup = BackupUtil.decodeBackup(context, uri)
restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs
// Store source mapping for error messages // Store source mapping for error messages
val backupMaps = backup.backupSources + backup.backupBrokenSources.map { it.toBackupSource() } val backupMaps = backup.backupSources + backup.backupBrokenSources.map { it.toBackupSource() }
sourceMapping = backupMaps.associate { it.sourceId to it.name } sourceMapping = backupMaps.associate { it.sourceId to it.name }
if (options.library) {
restoreAmount += backup.backupManga.size + 1 // +1 for categories
}
if (options.appSettings) {
restoreAmount += 1
}
if (options.sourceSettings) {
restoreAmount += 1
}
coroutineScope { coroutineScope {
if (options.library) {
restoreCategories(backup.backupCategories) restoreCategories(backup.backupCategories)
}
if (options.appSettings) {
restoreAppPreferences(backup.backupPreferences) restoreAppPreferences(backup.backupPreferences)
}
if (options.sourceSettings) {
restoreSourcePreferences(backup.backupSourcePreferences) restoreSourcePreferences(backup.backupSourcePreferences)
}
if (options.library) {
restoreManga(backup.backupManga, backup.backupCategories) restoreManga(backup.backupManga, backup.backupCategories)
}
// TODO: optionally trigger online library + tracker update // TODO: optionally trigger online library + tracker update
} }
@ -154,3 +170,19 @@ class BackupRestorer(
return File("") return File("")
} }
} }
data class RestoreOptions(
val appSettings: Boolean,
val sourceSettings: Boolean,
val library: Boolean,
) {
fun toBooleanArray() = booleanArrayOf(appSettings, sourceSettings, library)
companion object {
fun fromBooleanArray(booleanArray: BooleanArray) = RestoreOptions(
appSettings = booleanArray[0],
sourceSettings = booleanArray[1],
library = booleanArray[2],
)
}
}