This repository has been archived by the owner on Nov 5, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 712
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
GitHub backups + Material3 migration (#2507)
- GitHub backups MVP - Note: Auto-backups are NOT implemented, yet! - Migrate Material2 components to Material3. (verify that the UI still works and looks okay)
- Loading branch information
1 parent
eac28de
commit c938714
Showing
158 changed files
with
2,904 additions
and
1,514 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 9 additions & 0 deletions
9
app/src/main/java/com/ivy/wallet/backup/github/GitHubAutoBackupManager.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.ivy.wallet.backup.github | ||
|
||
import javax.inject.Inject | ||
|
||
class GitHubAutoBackupManager @Inject constructor() { | ||
fun scheduleAutoBackups() { | ||
// TODO: | ||
} | ||
} |
113 changes: 113 additions & 0 deletions
113
app/src/main/java/com/ivy/wallet/backup/github/GitHubBackup.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package com.ivy.wallet.backup.github | ||
|
||
import android.content.Context | ||
import androidx.datastore.preferences.core.edit | ||
import arrow.core.Either | ||
import arrow.core.raise.either | ||
import arrow.core.raise.ensureNotNull | ||
import com.ivy.wallet.data.DatastoreKeys | ||
import com.ivy.wallet.data.dataStore | ||
import com.ivy.wallet.domain.deprecated.logic.zip.ExportBackupLogic | ||
import dagger.hilt.android.qualifiers.ApplicationContext | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.MutableStateFlow | ||
import kotlinx.coroutines.flow.distinctUntilChanged | ||
import kotlinx.coroutines.flow.firstOrNull | ||
import kotlinx.coroutines.flow.map | ||
import kotlinx.coroutines.withContext | ||
import java.time.Instant | ||
import javax.inject.Inject | ||
|
||
class GitHubBackup @Inject constructor( | ||
private val client: GitHubClient, | ||
private val credentialsManager: GitHubCredentialsManager, | ||
private val exportBackupLogic: ExportBackupLogic, | ||
@ApplicationContext | ||
private val appContext: Context | ||
) { | ||
private val enabledInternalState = MutableStateFlow<Boolean?>(null) | ||
private val lastBackupTimeState = MutableStateFlow<Long?>(null) | ||
|
||
val enabled: Flow<Boolean> = enabledInternalState.map { state -> | ||
state ?: credentialsManager.getCredentials().isRight().also { | ||
enabledInternalState.value = it | ||
} | ||
}.distinctUntilChanged() | ||
|
||
val lastBackupTime: Flow<Instant?> = lastBackupTimeState.map { state -> | ||
val epochSeconds = state ?: appContext.dataStore.data | ||
.firstOrNull()?.let { it[DatastoreKeys.GITHUB_LAST_BACKUP_EPOCH_SEC] } | ||
|
||
epochSeconds?.let(Instant::ofEpochSecond) | ||
} | ||
|
||
suspend fun enable( | ||
gitHubUrl: String, | ||
gitHubPAT: String, | ||
): Either<String, Unit> = either { | ||
val regex = """https?://(?:www\.)?github\.com/([^/]+)/([^/]+)""".toRegex() | ||
val matchResult = regex.find(gitHubUrl) | ||
|
||
val owner = matchResult?.groups?.get(1)?.value | ||
ensureNotNull(owner) { | ||
"Couldn't parse 'owner' from \"$gitHubUrl.\"" | ||
} | ||
val repo = matchResult.groups[2]?.value | ||
ensureNotNull(repo) { | ||
"Couldn't parse 'repo' from \"$gitHubUrl.\"" | ||
} | ||
|
||
credentialsManager.saveCredentials( | ||
owner = owner, | ||
repo = repo, | ||
gitHubPAT = gitHubPAT, | ||
).bind() | ||
enabledInternalState.value = true | ||
} | ||
|
||
suspend fun disable() { | ||
credentialsManager.removeSaved() | ||
enabledInternalState.value = false | ||
} | ||
|
||
suspend fun repoUrl(): String? { | ||
return credentialsManager.getCredentials() | ||
.map { "https://github.com/${it.owner}/${it.repo}" } | ||
.getOrNull() | ||
} | ||
|
||
suspend fun backupData(): Either<Error, Unit> = withContext(Dispatchers.IO) { | ||
either { | ||
val credentials = credentialsManager.getCredentials() | ||
.mapLeft(Error::MissingCredentials).bind() | ||
|
||
val json = exportBackupLogic.generateJsonBackup() | ||
client.commit( | ||
credentials = credentials, | ||
path = "Ivy-Wallet-backup.json", | ||
content = json, | ||
).mapLeft(Error::Commit).bind() | ||
|
||
appContext.dataStore.edit { | ||
val epochSecondsNow = Instant.now().epochSecond | ||
it[DatastoreKeys.GITHUB_LAST_BACKUP_EPOCH_SEC] = epochSecondsNow | ||
lastBackupTimeState.value = epochSecondsNow | ||
} | ||
} | ||
} | ||
|
||
sealed interface Error { | ||
val humanReadable: String | ||
|
||
data class MissingCredentials(val error: String) : Error { | ||
override val humanReadable: String | ||
get() = "Missing credentials: $error." | ||
} | ||
|
||
data class Commit(val error: String) : Error { | ||
override val humanReadable: String | ||
get() = "Failed to commit: $error." | ||
} | ||
} | ||
} |
104 changes: 104 additions & 0 deletions
104
app/src/main/java/com/ivy/wallet/backup/github/GitHubClient.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package com.ivy.wallet.backup.github | ||
|
||
import androidx.annotation.Keep | ||
import arrow.core.Either | ||
import arrow.core.raise.catch | ||
import arrow.core.raise.either | ||
import arrow.core.raise.ensure | ||
import dagger.Lazy | ||
import io.ktor.client.HttpClient | ||
import io.ktor.client.call.body | ||
import io.ktor.client.request.get | ||
import io.ktor.client.request.headers | ||
import io.ktor.client.request.put | ||
import io.ktor.client.request.setBody | ||
import io.ktor.http.ContentType | ||
import io.ktor.http.HeadersBuilder | ||
import io.ktor.http.contentType | ||
import io.ktor.http.isSuccess | ||
import io.ktor.util.encodeBase64 | ||
import kotlinx.serialization.Serializable | ||
import javax.inject.Inject | ||
|
||
|
||
class GitHubClient @Inject constructor( | ||
private val httpClient: Lazy<HttpClient>, | ||
) { | ||
@Keep | ||
@Serializable | ||
data class GitHubFileContent( | ||
val content: String, | ||
val message: String, | ||
val committer: Committer, | ||
val sha: String?, | ||
) | ||
|
||
@Keep | ||
@Serializable | ||
data class Committer(val name: String, val email: String) | ||
|
||
@Keep | ||
@Serializable | ||
data class GitHubFileResponse(val sha: String) | ||
|
||
suspend fun commit( | ||
credentials: GitHubCredentials, | ||
path: String, | ||
content: String, | ||
): Either<String, Unit> = either { | ||
val url = repoUrl(credentials, path) | ||
val sha = getExistingFileSha(credentials, url) | ||
|
||
val encodedContent = content.toByteArray(Charsets.UTF_16).encodeBase64() | ||
|
||
val requestBody = GitHubFileContent( | ||
content = encodedContent, | ||
message = "Committing from Ktor", | ||
committer = Committer(name = "Ivy Wallet", email = "[email protected]"), | ||
sha = sha, | ||
) | ||
|
||
val response = httpClient.get().put(url) { | ||
headers { | ||
githubToken(credentials) | ||
contentType(ContentType.Application.Json) | ||
acceptsUtf16() | ||
} | ||
setBody(requestBody) | ||
} | ||
ensure(response.status.isSuccess()) { | ||
"Unsuccessful response: ${response.status}" | ||
} | ||
return Either.Right(Unit) | ||
} | ||
|
||
private suspend fun getExistingFileSha( | ||
credentials: GitHubCredentials, | ||
url: String | ||
): String? = catch({ | ||
// Fetch the current file to get its SHA | ||
httpClient.get().get(url) { | ||
headers { | ||
githubToken(credentials) | ||
} | ||
}.body<GitHubFileResponse>().sha | ||
|
||
}) { | ||
null | ||
} | ||
|
||
private fun HeadersBuilder.githubToken(credentials: GitHubCredentials) { | ||
append("Authorization", "token ${credentials.accessToken}") | ||
} | ||
|
||
private fun HeadersBuilder.acceptsUtf16() { | ||
append("Accept-Charset", "UTF-16") | ||
} | ||
|
||
private fun repoUrl( | ||
credentials: GitHubCredentials, | ||
path: String | ||
): String { | ||
return "https://api.github.com/repos/${credentials.owner}/${credentials.repo}/contents/$path" | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
app/src/main/java/com/ivy/wallet/backup/github/GitHubCredentials.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.ivy.wallet.backup.github | ||
|
||
data class GitHubCredentials( | ||
val owner: String, | ||
val repo: String, | ||
val accessToken: String, | ||
) |
Oops, something went wrong.