mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Backups
Restmore may be broken
This commit is contained in:
@@ -135,7 +135,6 @@ android {
|
|||||||
"META-INF/README.md",
|
"META-INF/README.md",
|
||||||
"META-INF/NOTICE",
|
"META-INF/NOTICE",
|
||||||
"META-INF/*.kotlin_module",
|
"META-INF/*.kotlin_module",
|
||||||
"META-INF/*.version",
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,11 +65,11 @@ subprojects {
|
|||||||
}
|
}
|
||||||
tasks.withType<org.jmailen.gradle.kotlinter.tasks.LintTask> {
|
tasks.withType<org.jmailen.gradle.kotlinter.tasks.LintTask> {
|
||||||
source(files("src"))
|
source(files("src"))
|
||||||
exclude("ca/gosyer/jui/*/build")
|
exclude("ca/gosyer/jui/*/build", "graphql")
|
||||||
}
|
}
|
||||||
tasks.withType<org.jmailen.gradle.kotlinter.tasks.FormatTask> {
|
tasks.withType<org.jmailen.gradle.kotlinter.tasks.FormatTask> {
|
||||||
source(files("src"))
|
source(files("src"))
|
||||||
exclude("ca/gosyer/jui/*/build")
|
exclude("ca/gosyer/jui/*/build", "ca/gosyer/jui/*/build")
|
||||||
}
|
}
|
||||||
plugins.withType<com.android.build.gradle.BasePlugin> {
|
plugins.withType<com.android.build.gradle.BasePlugin> {
|
||||||
configure<com.android.build.gradle.BaseExtension> {
|
configure<com.android.build.gradle.BaseExtension> {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
package ca.gosyer.jui.core.io
|
package ca.gosyer.jui.core.io
|
||||||
|
|
||||||
import ca.gosyer.jui.core.lang.withIOContext
|
import ca.gosyer.jui.core.lang.withIOContext
|
||||||
|
import io.ktor.utils.io.ByteReadChannel
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import okio.BufferedSink
|
import okio.BufferedSink
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
@@ -15,6 +16,8 @@ import okio.Path
|
|||||||
import okio.Source
|
import okio.Source
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.use
|
import okio.use
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
suspend fun Source.saveTo(path: Path) {
|
suspend fun Source.saveTo(path: Path) {
|
||||||
withIOContext {
|
withIOContext {
|
||||||
@@ -36,3 +39,5 @@ suspend fun Source.copyTo(sink: BufferedSink) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun ByteArray.source(): BufferedSource = Buffer().write(this)
|
fun ByteArray.source(): BufferedSource = Buffer().write(this)
|
||||||
|
|
||||||
|
expect suspend fun ByteReadChannel.toSource(context: CoroutineContext = EmptyCoroutineContext): Source
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.jui.ui.util.lang
|
package ca.gosyer.jui.core.io
|
||||||
|
|
||||||
import io.ktor.utils.io.ByteReadChannel
|
import io.ktor.utils.io.ByteReadChannel
|
||||||
import io.ktor.utils.io.cancel
|
import io.ktor.utils.io.cancel
|
||||||
@@ -17,7 +17,7 @@ import kotlin.coroutines.CoroutineContext
|
|||||||
|
|
||||||
actual suspend fun ByteReadChannel.toSource(context: CoroutineContext): Source {
|
actual suspend fun ByteReadChannel.toSource(context: CoroutineContext): Source {
|
||||||
val channel = this
|
val channel = this
|
||||||
return object : okio.Source {
|
return object : Source {
|
||||||
override fun close() {
|
override fun close() {
|
||||||
channel.cancel()
|
channel.cancel()
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package ca.gosyer.jui.core.io
|
||||||
|
|
||||||
|
import io.ktor.utils.io.ByteReadChannel
|
||||||
|
import io.ktor.utils.io.jvm.javaio.toInputStream
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import okio.Source
|
||||||
|
import okio.source
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
actual suspend fun ByteReadChannel.toSource(context: CoroutineContext): Source = toInputStream(context[Job]).source()
|
||||||
35
data/src/commonMain/graphql/Backup.graphql
Normal file
35
data/src/commonMain/graphql/Backup.graphql
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
fragment RestoreStatusFragment on BackupRestoreStatus {
|
||||||
|
state
|
||||||
|
mangaProgress
|
||||||
|
totalManga
|
||||||
|
}
|
||||||
|
|
||||||
|
query ValidateBackup($backup: Upload!) {
|
||||||
|
validateBackup(input: {backup: $backup}) {
|
||||||
|
missingSources {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation RestoreBackup($backup: Upload!) {
|
||||||
|
restoreBackup(input: {backup: $backup}) {
|
||||||
|
id
|
||||||
|
status {
|
||||||
|
...RestoreStatusFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query RestoreStatus($id: String!) {
|
||||||
|
restoreStatus(id: $id) {
|
||||||
|
...RestoreStatusFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation CreateBackup($includeCategories: Boolean!, $includeChapters: Boolean!) {
|
||||||
|
createBackup(input: {includeCategories: $includeCategories, includeChapters: $includeChapters}) {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,10 @@
|
|||||||
package ca.gosyer.jui.data
|
package ca.gosyer.jui.data
|
||||||
|
|
||||||
import ca.gosyer.jui.core.lang.addSuffix
|
import ca.gosyer.jui.core.lang.addSuffix
|
||||||
|
import ca.gosyer.jui.data.backup.BackupRepositoryImpl
|
||||||
import ca.gosyer.jui.data.chapter.ChapterRepositoryImpl
|
import ca.gosyer.jui.data.chapter.ChapterRepositoryImpl
|
||||||
import ca.gosyer.jui.data.settings.SettingsRepositoryImpl
|
import ca.gosyer.jui.data.settings.SettingsRepositoryImpl
|
||||||
|
import ca.gosyer.jui.domain.backup.service.BackupRepository
|
||||||
import ca.gosyer.jui.domain.chapter.service.ChapterRepository
|
import ca.gosyer.jui.domain.chapter.service.ChapterRepository
|
||||||
import ca.gosyer.jui.domain.server.Http
|
import ca.gosyer.jui.domain.server.Http
|
||||||
import ca.gosyer.jui.domain.server.service.ServerPreferences
|
import ca.gosyer.jui.domain.server.service.ServerPreferences
|
||||||
@@ -59,4 +61,11 @@ interface DataComponent : SharedDataComponent {
|
|||||||
http: Http,
|
http: Http,
|
||||||
serverPreferences: ServerPreferences,
|
serverPreferences: ServerPreferences,
|
||||||
): ChapterRepository = ChapterRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get())
|
): ChapterRepository = ChapterRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get())
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun backupRepository(
|
||||||
|
apolloClient: ApolloClient,
|
||||||
|
http: Http,
|
||||||
|
serverPreferences: ServerPreferences,
|
||||||
|
): BackupRepository = BackupRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package ca.gosyer.jui.data.backup
|
||||||
|
|
||||||
|
import ca.gosyer.jui.core.io.toSource
|
||||||
|
import ca.gosyer.jui.data.graphql.CreateBackupMutation
|
||||||
|
import ca.gosyer.jui.data.graphql.RestoreBackupMutation
|
||||||
|
import ca.gosyer.jui.data.graphql.RestoreStatusQuery
|
||||||
|
import ca.gosyer.jui.data.graphql.ValidateBackupQuery
|
||||||
|
import ca.gosyer.jui.data.graphql.fragment.RestoreStatusFragment
|
||||||
|
import ca.gosyer.jui.data.graphql.type.BackupRestoreState
|
||||||
|
import ca.gosyer.jui.domain.backup.model.BackupValidationResult
|
||||||
|
import ca.gosyer.jui.domain.backup.model.RestoreState
|
||||||
|
import ca.gosyer.jui.domain.backup.model.RestoreStatus
|
||||||
|
import ca.gosyer.jui.domain.backup.service.BackupRepository
|
||||||
|
import ca.gosyer.jui.domain.server.Http
|
||||||
|
import com.apollographql.apollo.ApolloClient
|
||||||
|
import com.apollographql.apollo.api.DefaultUpload
|
||||||
|
import io.ktor.client.request.HttpRequestBuilder
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.statement.bodyAsChannel
|
||||||
|
import io.ktor.http.Url
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import okio.Source
|
||||||
|
import okio.buffer
|
||||||
|
|
||||||
|
class BackupRepositoryImpl(
|
||||||
|
private val apolloClient: ApolloClient,
|
||||||
|
private val http: Http,
|
||||||
|
private val serverUrl: Url,
|
||||||
|
) : BackupRepository {
|
||||||
|
|
||||||
|
override fun validateBackup(source: Source): Flow<BackupValidationResult> {
|
||||||
|
return apolloClient.query(
|
||||||
|
ValidateBackupQuery(
|
||||||
|
DefaultUpload.Builder()
|
||||||
|
.content {
|
||||||
|
it.writeAll(source.buffer())
|
||||||
|
}
|
||||||
|
.fileName("backup.tachibk")
|
||||||
|
.contentType("application/octet-stream")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
).toFlow()
|
||||||
|
.map {
|
||||||
|
BackupValidationResult(
|
||||||
|
missingSources = it.dataAssertNoErrors.validateBackup.missingSources.map { source ->
|
||||||
|
"${source.name} (${source.id})"
|
||||||
|
},
|
||||||
|
missingTrackers = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun restoreBackup(source: Source): Flow<Pair<String, RestoreStatus>> {
|
||||||
|
return apolloClient.mutation(
|
||||||
|
RestoreBackupMutation(
|
||||||
|
DefaultUpload.Builder()
|
||||||
|
.content {
|
||||||
|
it.writeAll(source.buffer())
|
||||||
|
}
|
||||||
|
.fileName("backup.tachibk")
|
||||||
|
.contentType("application/octet-stream")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
).toFlow()
|
||||||
|
.map {
|
||||||
|
val data = it.dataAssertNoErrors
|
||||||
|
data.restoreBackup.id to data.restoreBackup.status!!
|
||||||
|
.restoreStatusFragment
|
||||||
|
.toRestoreStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun restoreStatus(id: String): Flow<RestoreStatus> {
|
||||||
|
return apolloClient.query(
|
||||||
|
RestoreStatusQuery(id)
|
||||||
|
).toFlow()
|
||||||
|
.map {
|
||||||
|
val data = it.dataAssertNoErrors
|
||||||
|
data.restoreStatus!!.restoreStatusFragment.toRestoreStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createBackup(
|
||||||
|
includeCategories: Boolean,
|
||||||
|
includeChapters: Boolean,
|
||||||
|
block: HttpRequestBuilder.() -> Unit,
|
||||||
|
): Flow<Pair<String, Source>> {
|
||||||
|
return apolloClient
|
||||||
|
.mutation(
|
||||||
|
CreateBackupMutation(includeCategories, includeChapters)
|
||||||
|
)
|
||||||
|
.toFlow()
|
||||||
|
.map {
|
||||||
|
val url = it.dataAssertNoErrors.createBackup.url
|
||||||
|
val response = http.get(
|
||||||
|
Url("$serverUrl${url}")
|
||||||
|
)
|
||||||
|
val fileName = response.headers["content-disposition"]!!
|
||||||
|
.substringAfter("filename=")
|
||||||
|
.trim('"')
|
||||||
|
fileName to response.bodyAsChannel().toSource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private fun RestoreStatusFragment.toRestoreStatus() = RestoreStatus(
|
||||||
|
when (state) {
|
||||||
|
BackupRestoreState.IDLE -> RestoreState.IDLE
|
||||||
|
BackupRestoreState.SUCCESS -> RestoreState.SUCCESS
|
||||||
|
BackupRestoreState.FAILURE -> RestoreState.FAILURE
|
||||||
|
BackupRestoreState.RESTORING_CATEGORIES -> RestoreState.RESTORING_CATEGORIES
|
||||||
|
BackupRestoreState.RESTORING_MANGA -> RestoreState.RESTORING_MANGA
|
||||||
|
BackupRestoreState.UNKNOWN__ -> RestoreState.UNKNOWN
|
||||||
|
},
|
||||||
|
mangaProgress,
|
||||||
|
totalManga,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
package ca.gosyer.jui.domain.backup.interactor
|
package ca.gosyer.jui.domain.backup.interactor
|
||||||
|
|
||||||
import ca.gosyer.jui.domain.backup.service.BackupRepositoryOld
|
import ca.gosyer.jui.domain.backup.service.BackupRepository
|
||||||
import io.ktor.client.request.HttpRequestBuilder
|
import io.ktor.client.request.HttpRequestBuilder
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.singleOrNull
|
import kotlinx.coroutines.flow.singleOrNull
|
||||||
@@ -16,19 +16,25 @@ import org.lighthousegames.logging.logging
|
|||||||
class ExportBackupFile
|
class ExportBackupFile
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val backupRepositoryOld: BackupRepositoryOld,
|
private val backupRepository: BackupRepository,
|
||||||
) {
|
) {
|
||||||
suspend fun await(
|
suspend fun await(
|
||||||
|
includeCategories: Boolean,
|
||||||
|
includeChapters: Boolean,
|
||||||
block: HttpRequestBuilder.() -> Unit = {},
|
block: HttpRequestBuilder.() -> Unit = {},
|
||||||
onError: suspend (Throwable) -> Unit = {},
|
onError: suspend (Throwable) -> Unit = {},
|
||||||
) = asFlow(block)
|
) = asFlow(includeCategories, includeChapters, block)
|
||||||
.catch {
|
.catch {
|
||||||
onError(it)
|
onError(it)
|
||||||
log.warn(it) { "Failed to export backup" }
|
log.warn(it) { "Failed to export backup" }
|
||||||
}
|
}
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
|
|
||||||
fun asFlow(block: HttpRequestBuilder.() -> Unit = {}) = backupRepositoryOld.exportBackupFile(block)
|
fun asFlow(
|
||||||
|
includeCategories: Boolean,
|
||||||
|
includeChapters: Boolean,
|
||||||
|
block: HttpRequestBuilder.() -> Unit = {},
|
||||||
|
) = backupRepository.createBackup(includeCategories, includeChapters, block)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = logging()
|
private val log = logging()
|
||||||
|
|||||||
@@ -6,24 +6,24 @@
|
|||||||
|
|
||||||
package ca.gosyer.jui.domain.backup.interactor
|
package ca.gosyer.jui.domain.backup.interactor
|
||||||
|
|
||||||
import ca.gosyer.jui.domain.backup.service.BackupRepositoryOld
|
import ca.gosyer.jui.domain.backup.service.BackupRepository
|
||||||
import io.ktor.client.request.HttpRequestBuilder
|
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.singleOrNull
|
import kotlinx.coroutines.flow.singleOrNull
|
||||||
import me.tatarka.inject.annotations.Inject
|
import me.tatarka.inject.annotations.Inject
|
||||||
|
import okio.FileSystem
|
||||||
import okio.Path
|
import okio.Path
|
||||||
|
import okio.SYSTEM
|
||||||
import org.lighthousegames.logging.logging
|
import org.lighthousegames.logging.logging
|
||||||
|
|
||||||
class ImportBackupFile
|
class ImportBackupFile
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val backupRepositoryOld: BackupRepositoryOld,
|
private val backupRepository: BackupRepository,
|
||||||
) {
|
) {
|
||||||
suspend fun await(
|
suspend fun await(
|
||||||
file: Path,
|
file: Path,
|
||||||
block: HttpRequestBuilder.() -> Unit = {},
|
|
||||||
onError: suspend (Throwable) -> Unit = {},
|
onError: suspend (Throwable) -> Unit = {},
|
||||||
) = asFlow(file, block)
|
) = asFlow(file)
|
||||||
.catch {
|
.catch {
|
||||||
onError(it)
|
onError(it)
|
||||||
log.warn(it) { "Failed to import backup ${file.name}" }
|
log.warn(it) { "Failed to import backup ${file.name}" }
|
||||||
@@ -32,8 +32,7 @@ class ImportBackupFile
|
|||||||
|
|
||||||
fun asFlow(
|
fun asFlow(
|
||||||
file: Path,
|
file: Path,
|
||||||
block: HttpRequestBuilder.() -> Unit = {},
|
) = backupRepository.restoreBackup(FileSystem.SYSTEM.source(file))
|
||||||
) = backupRepositoryOld.importBackupFile(BackupRepositoryOld.buildBackupFormData(file), block)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = logging()
|
private val log = logging()
|
||||||
|
|||||||
@@ -6,24 +6,24 @@
|
|||||||
|
|
||||||
package ca.gosyer.jui.domain.backup.interactor
|
package ca.gosyer.jui.domain.backup.interactor
|
||||||
|
|
||||||
import ca.gosyer.jui.domain.backup.service.BackupRepositoryOld
|
import ca.gosyer.jui.domain.backup.service.BackupRepository
|
||||||
import io.ktor.client.request.HttpRequestBuilder
|
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.singleOrNull
|
import kotlinx.coroutines.flow.singleOrNull
|
||||||
import me.tatarka.inject.annotations.Inject
|
import me.tatarka.inject.annotations.Inject
|
||||||
|
import okio.FileSystem
|
||||||
import okio.Path
|
import okio.Path
|
||||||
|
import okio.SYSTEM
|
||||||
import org.lighthousegames.logging.logging
|
import org.lighthousegames.logging.logging
|
||||||
|
|
||||||
class ValidateBackupFile
|
class ValidateBackupFile
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val backupRepositoryOld: BackupRepositoryOld,
|
private val backupRepository: BackupRepository,
|
||||||
) {
|
) {
|
||||||
suspend fun await(
|
suspend fun await(
|
||||||
file: Path,
|
file: Path,
|
||||||
block: HttpRequestBuilder.() -> Unit = {},
|
|
||||||
onError: suspend (Throwable) -> Unit = {},
|
onError: suspend (Throwable) -> Unit = {},
|
||||||
) = asFlow(file, block)
|
) = asFlow(file)
|
||||||
.catch {
|
.catch {
|
||||||
onError(it)
|
onError(it)
|
||||||
log.warn(it) { "Failed to validate backup ${file.name}" }
|
log.warn(it) { "Failed to validate backup ${file.name}" }
|
||||||
@@ -32,8 +32,7 @@ class ValidateBackupFile
|
|||||||
|
|
||||||
fun asFlow(
|
fun asFlow(
|
||||||
file: Path,
|
file: Path,
|
||||||
block: HttpRequestBuilder.() -> Unit = {},
|
) = backupRepository.validateBackup(FileSystem.SYSTEM.source(file))
|
||||||
) = backupRepositoryOld.validateBackupFile(BackupRepositoryOld.buildBackupFormData(file), block)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = logging()
|
private val log = logging()
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.jui.domain.backup.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
enum class RestoreState {
|
||||||
|
IDLE,
|
||||||
|
SUCCESS,
|
||||||
|
FAILURE,
|
||||||
|
RESTORING_CATEGORIES,
|
||||||
|
RESTORING_MANGA,
|
||||||
|
UNKNOWN,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RestoreStatus(
|
||||||
|
val state: RestoreState,
|
||||||
|
val completed: Int,
|
||||||
|
val total: Int,
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package ca.gosyer.jui.domain.backup.service
|
||||||
|
|
||||||
|
import ca.gosyer.jui.domain.backup.model.BackupValidationResult
|
||||||
|
import ca.gosyer.jui.domain.backup.model.RestoreStatus
|
||||||
|
import io.ktor.client.request.HttpRequestBuilder
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import okio.Source
|
||||||
|
|
||||||
|
interface BackupRepository {
|
||||||
|
fun validateBackup(source: Source): Flow<BackupValidationResult>
|
||||||
|
fun restoreBackup(source: Source): Flow<Pair<String, RestoreStatus>>
|
||||||
|
fun restoreStatus(id: String): Flow<RestoreStatus>
|
||||||
|
fun createBackup(
|
||||||
|
includeCategories: Boolean,
|
||||||
|
includeChapters: Boolean,
|
||||||
|
block: HttpRequestBuilder.() -> Unit,
|
||||||
|
): Flow<Pair<String, Source>>
|
||||||
|
}
|
||||||
@@ -18,19 +18,21 @@ import okio.Source
|
|||||||
import okio.source
|
import okio.source
|
||||||
|
|
||||||
actual class FileChooser(
|
actual class FileChooser(
|
||||||
private val resultLauncher: ManagedActivityResultLauncher<String, Uri?>,
|
private val resultLauncher: ManagedActivityResultLauncher<Array<String>, Uri?>,
|
||||||
) {
|
) {
|
||||||
actual fun launch(extension: String) {
|
actual fun launch(vararg extensions: String) {
|
||||||
val mime = MimeTypeMap.getSingleton()
|
val mimes = extensions.mapNotNull { extension ->
|
||||||
.getMimeTypeFromExtension(extension) ?: return
|
MimeTypeMap.getSingleton()
|
||||||
resultLauncher.launch(mime)
|
.getMimeTypeFromExtension(extension)
|
||||||
|
}.toTypedArray()
|
||||||
|
resultLauncher.launch(mimes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun rememberFileChooser(onFileFound: (Source) -> Unit): FileChooser {
|
actual fun rememberFileChooser(onFileFound: (Source) -> Unit): FileChooser {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val result = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
|
val result = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
context.contentResolver.openInputStream(it)?.source()?.let(onFileFound)
|
context.contentResolver.openInputStream(it)?.source()?.let(onFileFound)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import okio.Source
|
import okio.Source
|
||||||
|
|
||||||
expect class FileChooser {
|
expect class FileChooser {
|
||||||
fun launch(extension: String)
|
fun launch(vararg extensions: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ import ca.gosyer.jui.core.lang.throwIfCancellation
|
|||||||
import ca.gosyer.jui.domain.backup.interactor.ExportBackupFile
|
import ca.gosyer.jui.domain.backup.interactor.ExportBackupFile
|
||||||
import ca.gosyer.jui.domain.backup.interactor.ImportBackupFile
|
import ca.gosyer.jui.domain.backup.interactor.ImportBackupFile
|
||||||
import ca.gosyer.jui.domain.backup.interactor.ValidateBackupFile
|
import ca.gosyer.jui.domain.backup.interactor.ValidateBackupFile
|
||||||
|
import ca.gosyer.jui.domain.backup.model.RestoreState
|
||||||
|
import ca.gosyer.jui.domain.backup.model.RestoreStatus
|
||||||
import ca.gosyer.jui.i18n.MR
|
import ca.gosyer.jui.i18n.MR
|
||||||
import ca.gosyer.jui.ui.base.dialog.getMaterialDialogProperties
|
import ca.gosyer.jui.ui.base.dialog.getMaterialDialogProperties
|
||||||
import ca.gosyer.jui.ui.base.file.rememberFileChooser
|
import ca.gosyer.jui.ui.base.file.rememberFileChooser
|
||||||
@@ -53,7 +55,6 @@ import ca.gosyer.jui.ui.base.model.StableHolder
|
|||||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||||
import ca.gosyer.jui.ui.base.prefs.PreferenceRow
|
import ca.gosyer.jui.ui.base.prefs.PreferenceRow
|
||||||
import ca.gosyer.jui.ui.main.components.bottomNav
|
import ca.gosyer.jui.ui.main.components.bottomNav
|
||||||
import ca.gosyer.jui.ui.util.lang.toSource
|
|
||||||
import ca.gosyer.jui.ui.viewModel
|
import ca.gosyer.jui.ui.viewModel
|
||||||
import ca.gosyer.jui.uicore.components.VerticalScrollbar
|
import ca.gosyer.jui.uicore.components.VerticalScrollbar
|
||||||
import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter
|
import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter
|
||||||
@@ -70,8 +71,6 @@ import com.vanpra.composematerialdialogs.listItems
|
|||||||
import com.vanpra.composematerialdialogs.rememberMaterialDialogState
|
import com.vanpra.composematerialdialogs.rememberMaterialDialogState
|
||||||
import com.vanpra.composematerialdialogs.title
|
import com.vanpra.composematerialdialogs.title
|
||||||
import io.ktor.client.plugins.onDownload
|
import io.ktor.client.plugins.onDownload
|
||||||
import io.ktor.client.plugins.onUpload
|
|
||||||
import io.ktor.client.statement.bodyAsChannel
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@@ -172,19 +171,12 @@ class SettingsBackupViewModel
|
|||||||
|
|
||||||
fun restoreBackup(file: Path) {
|
fun restoreBackup(file: Path) {
|
||||||
importBackupFile
|
importBackupFile
|
||||||
.asFlow(file) {
|
.asFlow(file)
|
||||||
onUpload { bytesSentTotal, contentLength ->
|
|
||||||
_restoreStatus.value = Status.InProgress(
|
|
||||||
(bytesSentTotal.toFloat() / contentLength)
|
|
||||||
.coerceAtMost(1.0F),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onStart {
|
.onStart {
|
||||||
_restoreStatus.value = Status.InProgress(null)
|
_restoreStatus.value = Status.InProgress(null)
|
||||||
}
|
}
|
||||||
.onEach {
|
.onEach {
|
||||||
_restoreStatus.value = Status.Success
|
_restoreStatus.value = it.second.toStatus()
|
||||||
}
|
}
|
||||||
.catch {
|
.catch {
|
||||||
toast(it.message.orEmpty())
|
toast(it.message.orEmpty())
|
||||||
@@ -194,6 +186,15 @@ class SettingsBackupViewModel
|
|||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun RestoreStatus.toStatus() = when (state) {
|
||||||
|
RestoreState.IDLE -> Status.Success
|
||||||
|
RestoreState.SUCCESS -> Status.Success
|
||||||
|
RestoreState.FAILURE -> Status.Error
|
||||||
|
RestoreState.RESTORING_CATEGORIES -> Status.InProgress(0.01f)
|
||||||
|
RestoreState.RESTORING_MANGA -> Status.InProgress((completed.toFloat() / total).coerceIn(0f, 1f))
|
||||||
|
RestoreState.UNKNOWN -> Status.Error
|
||||||
|
}
|
||||||
|
|
||||||
fun stopRestore() {
|
fun stopRestore() {
|
||||||
_restoreStatus.value = Status.Error
|
_restoreStatus.value = Status.Error
|
||||||
}
|
}
|
||||||
@@ -203,7 +204,9 @@ class SettingsBackupViewModel
|
|||||||
|
|
||||||
fun exportBackup() {
|
fun exportBackup() {
|
||||||
exportBackupFile
|
exportBackupFile
|
||||||
.asFlow {
|
.asFlow(
|
||||||
|
true, true // todo
|
||||||
|
) {
|
||||||
onDownload { bytesSentTotal, contentLength ->
|
onDownload { bytesSentTotal, contentLength ->
|
||||||
_creatingStatus.value = Status.InProgress(
|
_creatingStatus.value = Status.InProgress(
|
||||||
(bytesSentTotal.toFloat() / contentLength)
|
(bytesSentTotal.toFloat() / contentLength)
|
||||||
@@ -214,15 +217,12 @@ class SettingsBackupViewModel
|
|||||||
.onStart {
|
.onStart {
|
||||||
_creatingStatus.value = Status.InProgress(null)
|
_creatingStatus.value = Status.InProgress(null)
|
||||||
}
|
}
|
||||||
.onEach { backup ->
|
.onEach { (filename, source) ->
|
||||||
val filename =
|
|
||||||
backup.headers["content-disposition"]?.substringAfter("filename=")
|
|
||||||
?.trim('"') ?: "backup"
|
|
||||||
tempFile.value = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(filename).also {
|
tempFile.value = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(filename).also {
|
||||||
mutex.tryLock()
|
mutex.tryLock()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
backup.bodyAsChannel().toSource().saveTo(it)
|
source.saveTo(it)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.throwIfCancellation()
|
e.throwIfCancellation()
|
||||||
log.warn(e) { "Error creating backup" }
|
log.warn(e) { "Error creating backup" }
|
||||||
@@ -338,7 +338,7 @@ private fun SettingsBackupScreenContent(
|
|||||||
stringResource(MR.strings.backup_restore_sub),
|
stringResource(MR.strings.backup_restore_sub),
|
||||||
restoreStatus,
|
restoreStatus,
|
||||||
) {
|
) {
|
||||||
fileChooser.launch("gz")
|
fileChooser.launch("gz", "tachibk")
|
||||||
}
|
}
|
||||||
PreferenceFile(
|
PreferenceFile(
|
||||||
stringResource(MR.strings.backup_create),
|
stringResource(MR.strings.backup_create),
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
/*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ca.gosyer.jui.ui.util.lang
|
|
||||||
|
|
||||||
import io.ktor.utils.io.ByteReadChannel
|
|
||||||
import okio.Source
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
|
|
||||||
expect suspend fun ByteReadChannel.toSource(context: CoroutineContext = EmptyCoroutineContext): Source
|
|
||||||
@@ -26,9 +26,9 @@ actual class FileChooser(
|
|||||||
details?.actionPerformed(null)
|
details?.actionPerformed(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun launch(extension: String) {
|
actual fun launch(vararg extensions: String) {
|
||||||
scope.launchDefault {
|
scope.launchDefault {
|
||||||
fileChooser.fileFilter = FileNameExtensionFilter("$extension file", extension)
|
fileChooser.fileFilter = FileNameExtensionFilter("${extensions.joinToString()} files", *extensions)
|
||||||
when (fileChooser.showOpenDialog(null)) {
|
when (fileChooser.showOpenDialog(null)) {
|
||||||
JFileChooser.APPROVE_OPTION -> onFileFound(fileChooser.selectedFile.source())
|
JFileChooser.APPROVE_OPTION -> onFileFound(fileChooser.selectedFile.source())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
/*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ca.gosyer.jui.ui.util.lang
|
|
||||||
|
|
||||||
import io.ktor.utils.io.ByteReadChannel
|
|
||||||
import io.ktor.utils.io.jvm.javaio.toInputStream
|
|
||||||
import okio.Source
|
|
||||||
import okio.source
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
|
|
||||||
actual suspend fun ByteReadChannel.toSource(context: CoroutineContext): Source = toInputStream().source()
|
|
||||||
Reference in New Issue
Block a user