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 auto-backups MVP (not tested) (#2509)
* Support JSON file import * WIP: GitHub backups * WIP: GitHub backups UX * WIP: `GitHubAutoBackupManager` * POC of the GitHub auto-backups * WIP: GitHubBackupCard screen * Import GitHub backup imports
- Loading branch information
1 parent
03e2d9d
commit e8f921e
Showing
15 changed files
with
543 additions
and
212 deletions.
There are no files selected for viewing
87 changes: 84 additions & 3 deletions
87
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 |
---|---|---|
@@ -1,9 +1,90 @@ | ||
package com.ivy.wallet.backup.github | ||
|
||
import android.content.Context | ||
import androidx.hilt.work.HiltWorker | ||
import androidx.work.Constraints | ||
import androidx.work.CoroutineWorker | ||
import androidx.work.ExistingPeriodicWorkPolicy | ||
import androidx.work.NetworkType | ||
import androidx.work.PeriodicWorkRequestBuilder | ||
import androidx.work.WorkManager | ||
import androidx.work.WorkerParameters | ||
import dagger.assisted.Assisted | ||
import dagger.assisted.AssistedInject | ||
import dagger.hilt.android.qualifiers.ApplicationContext | ||
import java.util.Calendar | ||
import java.util.concurrent.TimeUnit | ||
import javax.inject.Inject | ||
|
||
class GitHubAutoBackupManager @Inject constructor() { | ||
class GitHubAutoBackupManager @Inject constructor( | ||
@ApplicationContext | ||
private val context: Context | ||
) { | ||
|
||
private val uniqueWorkName = "GITHUB_AUTO_BACKUP_WORK" | ||
|
||
fun scheduleAutoBackups() { | ||
// TODO: | ||
val initialDelay = calculateInitialDelayMillis() | ||
|
||
val dailyWorkRequest = PeriodicWorkRequestBuilder<GitHubBackupWorker>( | ||
24, TimeUnit.HOURS | ||
).setInitialDelay(initialDelay, TimeUnit.MILLISECONDS) | ||
.setConstraints( | ||
Constraints.Builder() | ||
.setRequiredNetworkType(NetworkType.CONNECTED) | ||
.build() | ||
) | ||
.build() | ||
|
||
WorkManager.getInstance(context) | ||
.enqueueUniquePeriodicWork( | ||
uniqueWorkName, | ||
ExistingPeriodicWorkPolicy.REPLACE, | ||
dailyWorkRequest | ||
) | ||
} | ||
|
||
private fun calculateInitialDelayMillis(): Long { | ||
val lunchTime = Calendar.getInstance().apply { | ||
set(Calendar.HOUR_OF_DAY, 12) | ||
set(Calendar.MINUTE, 0) | ||
set(Calendar.SECOND, 0) | ||
} | ||
|
||
val currentTime = Calendar.getInstance() | ||
|
||
return if (currentTime.after(lunchTime)) { | ||
// If current time is after 12 pm, schedule for next day | ||
lunchTime.add(Calendar.DAY_OF_YEAR, 1) | ||
lunchTime.timeInMillis - currentTime.timeInMillis | ||
} else { | ||
// If it's before 12 pm, set delay to reach 12 pm | ||
lunchTime.timeInMillis - currentTime.timeInMillis | ||
} | ||
} | ||
|
||
fun cancelAutoBackups() { | ||
WorkManager.getInstance(context).cancelUniqueWork(uniqueWorkName) | ||
} | ||
} | ||
|
||
@HiltWorker | ||
class GitHubBackupWorker @AssistedInject constructor( | ||
@Assisted appContext: Context, | ||
@Assisted params: WorkerParameters, | ||
private val gitHubBackup: GitHubBackup, | ||
) : CoroutineWorker(appContext, params) { | ||
|
||
override suspend fun doWork(): Result { | ||
return gitHubBackup.backupData( | ||
isAutomatic = true | ||
).fold( | ||
ifLeft = { | ||
Result.failure() | ||
}, | ||
ifRight = { | ||
Result.success() | ||
}, | ||
) | ||
} | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ import arrow.core.Either | |
import arrow.core.raise.catch | ||
import arrow.core.raise.either | ||
import arrow.core.raise.ensure | ||
import arrow.core.raise.ensureNotNull | ||
import dagger.Lazy | ||
import io.ktor.client.HttpClient | ||
import io.ktor.client.call.body | ||
|
@@ -17,7 +18,9 @@ import io.ktor.http.HeadersBuilder | |
import io.ktor.http.contentType | ||
import io.ktor.http.isSuccess | ||
import io.ktor.util.encodeBase64 | ||
import kotlinx.serialization.SerialName | ||
import kotlinx.serialization.Serializable | ||
import timber.log.Timber | ||
import javax.inject.Inject | ||
|
||
|
||
|
@@ -39,26 +42,38 @@ class GitHubClient @Inject constructor( | |
|
||
@Keep | ||
@Serializable | ||
data class GitHubFileResponse(val sha: String) | ||
data class GitHubFileResponse( | ||
val sha: String, | ||
@SerialName("download_url") | ||
val downloadUrl: String, | ||
) | ||
|
||
suspend fun commit( | ||
credentials: GitHubCredentials, | ||
path: String, | ||
content: String, | ||
isAutomatic: Boolean = false, | ||
): Either<String, Unit> = either { | ||
val url = repoUrl(credentials, path) | ||
val sha = getExistingFileSha(credentials, url) | ||
val repoUrl = repoUrl(credentials, path) | ||
val sha = getExistingFile(credentials, repoUrl)?.sha | ||
|
||
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]"), | ||
message = if (isAutomatic) { | ||
"Automatic Ivy Wallet data backup" | ||
} else { | ||
"Manual Ivy Wallet data backup" | ||
}, | ||
committer = Committer( | ||
name = "Ivy Wallet", | ||
email = "[email protected]" | ||
), | ||
sha = sha, | ||
) | ||
|
||
val response = httpClient.get().put(url) { | ||
val response = httpClient.get().put(repoUrl) { | ||
headers { | ||
githubToken(credentials) | ||
contentType(ContentType.Application.Json) | ||
|
@@ -67,28 +82,70 @@ class GitHubClient @Inject constructor( | |
setBody(requestBody) | ||
} | ||
ensure(response.status.isSuccess()) { | ||
"Unsuccessful response: ${response.status}" | ||
when (response.status.value) { | ||
404 -> "Invalid GitHub repo url." | ||
403 -> "Invalid GitHub PAT (Personal Access Token). Check your PAT permissions and expiration day." | ||
else -> "Unsuccessful response: '${response.status}' $response." | ||
} | ||
} | ||
return Either.Right(Unit) | ||
} | ||
|
||
private suspend fun getExistingFileSha( | ||
suspend fun readFileContent( | ||
credentials: GitHubCredentials, | ||
path: String, | ||
): Either<String, String> = either { | ||
val repoUrl = repoUrl(credentials, path) | ||
val file = getExistingFile(credentials, repoUrl) | ||
ensureNotNull(file) { | ||
"Failed to fetch GitHub file '$repoUrl'." | ||
} | ||
val fileContent = downloadFileContent(credentials, file.downloadUrl).bind() | ||
ensure(fileContent.isNotBlank()) { | ||
"GitHub file content is blank!" | ||
} | ||
fileContent | ||
} | ||
|
||
private suspend fun downloadFileContent( | ||
credentials: GitHubCredentials, | ||
downloadUrl: String | ||
): Either<String, String> = catch({ | ||
val client = httpClient.get() | ||
val response = client.get(downloadUrl) { | ||
headers { | ||
githubToken(credentials) | ||
} | ||
} | ||
if (!response.status.isSuccess()) { | ||
error("Failed to download file with ${response.status} $response") | ||
} | ||
val byteArray = response.body<ByteArray>() | ||
|
||
val content = byteArray.toString(Charsets.UTF_16) | ||
Either.Right(content) | ||
}) { | ||
Timber.e("GitHub file download: $it") | ||
Either.Left("Failed to GitHub backup file because $it.") | ||
} | ||
|
||
private suspend fun getExistingFile( | ||
credentials: GitHubCredentials, | ||
url: String | ||
): String? = catch({ | ||
): GitHubFileResponse? = catch({ | ||
// Fetch the current file to get its SHA | ||
httpClient.get().get(url) { | ||
headers { | ||
githubToken(credentials) | ||
} | ||
}.body<GitHubFileResponse>().sha | ||
}.body<GitHubFileResponse>() | ||
|
||
}) { | ||
null | ||
} | ||
|
||
private fun HeadersBuilder.githubToken(credentials: GitHubCredentials) { | ||
append("Authorization", "token ${credentials.accessToken}") | ||
append("Authorization", "token ${credentials.gitHubPAT}") | ||
} | ||
|
||
private fun HeadersBuilder.acceptsUtf16() { | ||
|
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
Oops, something went wrong.