Cleanup sync code

This commit is contained in:
Jobobby04 2024-03-16 13:14:40 -04:00
parent 54cb379a50
commit d70258b956
3 changed files with 73 additions and 67 deletions

View File

@ -19,6 +19,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoBuf
import logcat.LogPriority import logcat.LogPriority
import logcat.logcat import logcat.logcat
import tachiyomi.core.common.util.system.logcat
import tachiyomi.data.Chapters import tachiyomi.data.Chapters
import tachiyomi.data.DatabaseHandler import tachiyomi.data.DatabaseHandler
import tachiyomi.data.manga.MangaMapper.mapManga import tachiyomi.data.manga.MangaMapper.mapManga
@ -196,7 +197,7 @@ class SyncManager(
Uri.fromFile(cacheFile) Uri.fromFile(cacheFile)
} }
} catch (e: IOException) { } catch (e: IOException) {
logcat(LogPriority.ERROR) { "Failed to write sync data to cache" } logcat(LogPriority.ERROR, throwable = e) { "Failed to write sync data to cache" }
null null
} }
} }

View File

@ -4,7 +4,6 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.util.Log
import com.google.api.client.auth.oauth2.TokenResponseException import com.google.api.client.auth.oauth2.TokenResponseException
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest
@ -19,21 +18,19 @@ import com.google.api.services.drive.Drive
import com.google.api.services.drive.DriveScopes import com.google.api.services.drive.DriveScopes
import com.google.api.services.drive.model.File import com.google.api.services.drive.model.File
import eu.kanade.domain.sync.SyncPreferences import eu.kanade.domain.sync.SyncPreferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import logcat.LogPriority import logcat.LogPriority
import logcat.logcat import logcat.logcat
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStreamReader
import java.time.Instant import java.time.Instant
import java.util.zip.GZIPInputStream import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream import java.util.zip.GZIPOutputStream
@ -118,8 +115,8 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
throw Exception(context.stringResource(MR.strings.error_before_sync_gdrive) + ": Max retries reached.") throw Exception(context.stringResource(MR.strings.error_before_sync_gdrive) + ": Max retries reached.")
} }
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR) { "Error in GoogleDrive beforeSync: ${e.message}" } logcat(LogPriority.ERROR, throwable = e) { "Error in GoogleDrive beforeSync" }
throw Exception(context.stringResource(MR.strings.error_before_sync_gdrive) + ": ${e.message}") throw Exception(context.stringResource(MR.strings.error_before_sync_gdrive) + ": ${e.message}", e)
} }
} }
@ -145,22 +142,23 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
drive.files().get(gdriveFileId).executeMediaAndDownloadTo(outputStream) drive.files().get(gdriveFileId).executeMediaAndDownloadTo(outputStream)
logcat(LogPriority.DEBUG) { "File downloaded successfully" } logcat(LogPriority.DEBUG) { "File downloaded successfully" }
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR) { "Error downloading file: ${e.message}" } logcat(LogPriority.ERROR, throwable = e) { "Error downloading file" }
return null return null
} }
return withContext(Dispatchers.IO) { return withIOContext {
try { try {
val gzipInputStream = GZIPInputStream(ByteArrayInputStream(outputStream.toByteArray())) val gzipInputStream = GZIPInputStream(outputStream.toByteArray().inputStream())
val jsonString = gzipInputStream.bufferedReader(Charsets.UTF_8).use { it.readText() } val jsonString = gzipInputStream.bufferedReader().use { it.readText() }
val syncData = json.decodeFromString(SyncData.serializer(), jsonString) val syncData = json.decodeFromString(SyncData.serializer(), jsonString)
logcat(LogPriority.DEBUG) { "JSON deserialized successfully" } this@GoogleDriveSyncService.logcat(LogPriority.DEBUG) { "JSON deserialized successfully" }
syncData syncData
} catch (e: Exception) { } catch (e: Exception) {
logcat( this@GoogleDriveSyncService.logcat(
LogPriority.ERROR, LogPriority.ERROR,
) { "Failed to convert json to sync data with kotlinx.serialization: ${e.message}" } throwable = e,
throw Exception(e.message) ) { "Failed to convert json to sync data with kotlinx.serialization" }
throw Exception(e.message, e)
} }
} }
} }
@ -172,11 +170,11 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
val fileList = getAppDataFileList(drive) val fileList = getAppDataFileList(drive)
val byteArrayOutputStream = ByteArrayOutputStream() val byteArrayOutputStream = ByteArrayOutputStream()
withContext(Dispatchers.IO) { withIOContext {
GZIPOutputStream(byteArrayOutputStream).use { gzipOutputStream -> GZIPOutputStream(byteArrayOutputStream).use { gzipOutputStream ->
gzipOutputStream.write(jsonData.toByteArray(Charsets.UTF_8)) gzipOutputStream.write(jsonData.toByteArray(Charsets.UTF_8))
} }
logcat(LogPriority.DEBUG) { "JSON serialized successfully" } this@GoogleDriveSyncService.logcat(LogPriority.DEBUG) { "JSON serialized successfully" }
} }
val byteArrayContent = ByteArrayContent("application/octet-stream", byteArrayOutputStream.toByteArray()) val byteArrayContent = ByteArrayContent("application/octet-stream", byteArrayOutputStream.toByteArray())
@ -206,8 +204,8 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
// Data has been successfully pushed or updated, delete the lock file // Data has been successfully pushed or updated, delete the lock file
deleteLockFile(drive) deleteLockFile(drive)
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR) { "Failed to push or update sync data: ${e.message}" } logcat(LogPriority.ERROR, throwable = e) { "Failed to push or update sync data" }
throw Exception(context.stringResource(MR.strings.error_uploading_sync_data) + ": ${e.message}") throw Exception(context.stringResource(MR.strings.error_uploading_sync_data) + ": ${e.message}", e)
} }
} }
@ -222,11 +220,11 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
.setFields("files(id, name, createdTime)") .setFields("files(id, name, createdTime)")
.execute() .execute()
.files .files
Log.d("GoogleDrive", "AppData folder file list: $fileList") logcat { "AppData folder file list: $fileList" }
return fileList return fileList
} catch (e: Exception) { } catch (e: Exception) {
Log.e("GoogleDrive", "Error no sync data found in appData folder: ${e.message}") logcat(LogPriority.ERROR, throwable = e) { "Error no sync data found in appData folder" }
return mutableListOf() return mutableListOf()
} }
} }
@ -246,15 +244,15 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
.setFields("id, name, createdTime") .setFields("id, name, createdTime")
.execute() .execute()
Log.d("GoogleDrive", "Created lock file with ID: ${file.id}") logcat { "Created lock file with ID: ${file.id}" }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("GoogleDrive", "Error creating lock file: ${e.message}") logcat(LogPriority.ERROR, throwable = e) { "Error creating lock file" }
throw Exception(e.message) throw Exception(e.message, e)
} }
} }
private fun findLockFile(drive: Drive): MutableList<File> { private fun findLockFile(drive: Drive): MutableList<File> {
try { return try {
val query = "mimeType='text/plain' and name = '$lockFileName'" val query = "mimeType='text/plain' and name = '$lockFileName'"
val fileList = drive.files() val fileList = drive.files()
.list() .list()
@ -262,11 +260,11 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
.setQ(query) .setQ(query)
.setFields("files(id, name, createdTime)") .setFields("files(id, name, createdTime)")
.execute().files .execute().files
Log.d("GoogleDrive", "Lock file search result: $fileList") logcat { "Lock file search result: $fileList" }
return fileList fileList
} catch (e: Exception) { } catch (e: Exception) {
Log.e("GoogleDrive", "Error finding lock file: ${e.message}") logcat(LogPriority.ERROR, throwable = e) { "Error finding lock file" }
return mutableListOf() mutableListOf()
} }
} }
@ -277,14 +275,14 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
if (lockFiles.isNotEmpty()) { if (lockFiles.isNotEmpty()) {
for (file in lockFiles) { for (file in lockFiles) {
drive.files().delete(file.id).execute() drive.files().delete(file.id).execute()
Log.d("GoogleDrive", "Deleted lock file with ID: ${file.id}") logcat { "Deleted lock file with ID: ${file.id}" }
} }
} else { } else {
Log.d("GoogleDrive", "No lock file found to delete.") logcat { "No lock file found to delete." }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("GoogleDrive", "Error deleting lock file: ${e.message}") logcat(LogPriority.ERROR, throwable = e) { "Error deleting lock file" }
throw Exception(context.stringResource(MR.strings.error_deleting_google_drive_lock_file)) throw Exception(context.stringResource(MR.strings.error_deleting_google_drive_lock_file), e)
} }
} }
@ -297,24 +295,27 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
} }
googleDriveService.refreshToken() googleDriveService.refreshToken()
return withContext(Dispatchers.IO) { return withIOContext {
try { try {
val appDataFileList = getAppDataFileList(drive) val appDataFileList = getAppDataFileList(drive)
if (appDataFileList.isEmpty()) { if (appDataFileList.isEmpty()) {
logcat(LogPriority.DEBUG) { "No sync data file found in appData folder of Google Drive" } this@GoogleDriveSyncService
.logcat(LogPriority.DEBUG) { "No sync data file found in appData folder of Google Drive" }
DeleteSyncDataStatus.NO_FILES DeleteSyncDataStatus.NO_FILES
} else { } else {
for (file in appDataFileList) { for (file in appDataFileList) {
drive.files().delete(file.id).execute() drive.files().delete(file.id).execute()
logcat( this@GoogleDriveSyncService.logcat(
LogPriority.DEBUG, LogPriority.DEBUG,
) { "Deleted sync data file in appData folder of Google Drive with file ID: ${file.id}" } ) { "Deleted sync data file in appData folder of Google Drive with file ID: ${file.id}" }
} }
DeleteSyncDataStatus.SUCCESS DeleteSyncDataStatus.SUCCESS
} }
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR) { "Error occurred while interacting with Google Drive: ${e.message}" } this@GoogleDriveSyncService.logcat(LogPriority.ERROR, throwable = e) {
"Error occurred while interacting with Google Drive"
}
DeleteSyncDataStatus.ERROR DeleteSyncDataStatus.ERROR
} }
} }
@ -374,7 +375,7 @@ class GoogleDriveService(private val context: Context) {
val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance() val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()
val secrets = GoogleClientSecrets.load( val secrets = GoogleClientSecrets.load(
jsonFactory, jsonFactory,
InputStreamReader(context.assets.open("client_secrets.json")), context.assets.open("client_secrets.json").reader(),
) )
val flow = GoogleAuthorizationCodeFlow.Builder( val flow = GoogleAuthorizationCodeFlow.Builder(
@ -389,14 +390,14 @@ class GoogleDriveService(private val context: Context) {
.setApprovalPrompt("force") .setApprovalPrompt("force")
.build() .build()
} }
internal suspend fun refreshToken() = withContext(Dispatchers.IO) { internal suspend fun refreshToken() = withIOContext {
val refreshToken = syncPreferences.googleDriveRefreshToken().get() val refreshToken = syncPreferences.googleDriveRefreshToken().get()
val accessToken = syncPreferences.googleDriveAccessToken().get() val accessToken = syncPreferences.googleDriveAccessToken().get()
val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance() val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()
val secrets = GoogleClientSecrets.load( val secrets = GoogleClientSecrets.load(
jsonFactory, jsonFactory,
InputStreamReader(context.assets.open("client_secrets.json")), context.assets.open("client_secrets.json").reader(),
) )
val credential = GoogleCredential.Builder() val credential = GoogleCredential.Builder()
@ -411,7 +412,7 @@ class GoogleDriveService(private val context: Context) {
credential.refreshToken = refreshToken credential.refreshToken = refreshToken
logcat(LogPriority.DEBUG) { "Refreshing access token with: $refreshToken" } this@GoogleDriveService.logcat(LogPriority.DEBUG) { "Refreshing access token with: $refreshToken" }
try { try {
credential.refreshToken() credential.refreshToken()
@ -419,23 +420,26 @@ class GoogleDriveService(private val context: Context) {
// Save the new access token // Save the new access token
syncPreferences.googleDriveAccessToken().set(newAccessToken) syncPreferences.googleDriveAccessToken().set(newAccessToken)
setupGoogleDriveService(newAccessToken, credential.refreshToken) setupGoogleDriveService(newAccessToken, credential.refreshToken)
logcat(LogPriority.DEBUG) { "Google Access token refreshed old: $accessToken new: $newAccessToken" } this@GoogleDriveService
.logcat(LogPriority.DEBUG) { "Google Access token refreshed old: $accessToken new: $newAccessToken" }
} catch (e: TokenResponseException) { } catch (e: TokenResponseException) {
if (e.details.error == "invalid_grant") { if (e.details.error == "invalid_grant") {
// The refresh token is invalid, prompt the user to sign in again // The refresh token is invalid, prompt the user to sign in again
logcat(LogPriority.ERROR) { "Refresh token is invalid, prompt user to sign in again" } this@GoogleDriveService.logcat(LogPriority.ERROR, throwable = e) {
throw e.message?.let { Exception(it) } ?: Exception("Unknown error") "Refresh token is invalid, prompt user to sign in again"
}
throw e.message?.let { Exception(it, e) } ?: Exception("Unknown error", e)
} else { } else {
// Token refresh failed; handle this situation // Token refresh failed; handle this situation
logcat(LogPriority.ERROR) { "Failed to refresh access token ${e.message}" } this@GoogleDriveService.logcat(LogPriority.ERROR) { "Failed to refresh access token ${e.message}" }
logcat(LogPriority.ERROR) { "Google Drive sync will be disabled" } this@GoogleDriveService.logcat(LogPriority.ERROR) { "Google Drive sync will be disabled" }
throw e.message?.let { Exception(it) } ?: Exception("Unknown error") throw e.message?.let { Exception(it, e) } ?: Exception("Unknown error", e)
} }
} catch (e: IOException) { } catch (e: IOException) {
// Token refresh failed; handle this situation // Token refresh failed; handle this situation
logcat(LogPriority.ERROR) { "Failed to refresh access token ${e.message}" } this@GoogleDriveService.logcat(LogPriority.ERROR, throwable = e) { "Failed to refresh access token" }
logcat(LogPriority.ERROR) { "Google Drive sync will be disabled" } this@GoogleDriveService.logcat(LogPriority.ERROR) { "Google Drive sync will be disabled" }
throw e.message?.let { Exception(it) } ?: Exception("Unknown error") throw e.message?.let { Exception(it, e) } ?: Exception("Unknown error", e)
} }
} }
@ -448,7 +452,7 @@ class GoogleDriveService(private val context: Context) {
val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance() val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()
val secrets = GoogleClientSecrets.load( val secrets = GoogleClientSecrets.load(
jsonFactory, jsonFactory,
InputStreamReader(context.assets.open("client_secrets.json")), context.assets.open("client_secrets.json").reader(),
) )
val credential = GoogleCredential.Builder() val credential = GoogleCredential.Builder()
@ -487,7 +491,7 @@ class GoogleDriveService(private val context: Context) {
val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance() val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()
val secrets = GoogleClientSecrets.load( val secrets = GoogleClientSecrets.load(
jsonFactory, jsonFactory,
InputStreamReader(context.assets.open("client_secrets.json")), context.assets.open("client_secrets.json").reader(),
) )
val tokenResponse: GoogleTokenResponse = GoogleAuthorizationCodeTokenRequest( val tokenResponse: GoogleTokenResponse = GoogleAuthorizationCodeTokenRequest(
@ -515,6 +519,7 @@ class GoogleDriveService(private val context: Context) {
onSuccess() onSuccess()
} }
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, throwable = e) { "Failed to handle authorization code" }
activity.runOnUiThread { activity.runOnUiThread {
onFailure(e.localizedMessage ?: "Unknown error") onFailure(e.localizedMessage ?: "Unknown error")
} }

View File

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.sync.SyncNotifier
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.PATCH import eu.kanade.tachiyomi.network.PATCH
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -104,15 +105,15 @@ class SyncYomiSyncService(
) )
// create lock file first // create lock file first
client.newCall(lockFileCreate).execute() client.newCall(lockFileCreate).await()
// update lock file acquired_by // update lock file acquired_by
client.newCall(lockFileUpdate).execute() client.newCall(lockFileUpdate).await()
var backoff = 2000L // Start with 2 seconds var backoff = 2000L // Start with 2 seconds
val maxBackoff = 32000L // Maximum backoff time e.g., 32 seconds val maxBackoff = 32000L // Maximum backoff time e.g., 32 seconds
var lockFile: LockFile var lockFile: LockFile
do { do {
val response = client.newCall(lockFileRequest).execute() val response = client.newCall(lockFileRequest).await()
val responseBody = response.body.string() val responseBody = response.body.string()
lockFile = json.decodeFromString<LockFile>(responseBody) lockFile = json.decodeFromString<LockFile>(responseBody)
logcat(LogPriority.DEBUG) { "SyncYomi lock file status: ${lockFile.status}" } logcat(LogPriority.DEBUG) { "SyncYomi lock file status: ${lockFile.status}" }
@ -125,7 +126,7 @@ class SyncYomiSyncService(
} while (lockFile.status != SyncStatus.Success) } while (lockFile.status != SyncStatus.Success)
// update lock file acquired_by // update lock file acquired_by
client.newCall(lockFileUpdate).execute() client.newCall(lockFileUpdate).await()
} }
override suspend fun pullSyncData(): SyncData? { override suspend fun pullSyncData(): SyncData? {
@ -141,16 +142,15 @@ class SyncYomiSyncService(
headers = headers, headers = headers,
) )
client.newCall(downloadRequest).execute().use { response -> val response = client.newCall(downloadRequest).await()
val responseBody = response.body.string() val responseBody = response.body.string()
if (response.isSuccessful) { return if (response.isSuccessful) {
return json.decodeFromString<SyncData>(responseBody) json.decodeFromString<SyncData>(responseBody)
} else { } else {
notifier.showSyncError("Failed to download sync data: $responseBody") notifier.showSyncError("Failed to download sync data: $responseBody")
responseBody.let { logcat(LogPriority.ERROR) { "SyncError:$it" } } responseBody.let { logcat(LogPriority.ERROR) { "SyncError:$it" } }
return null null
}
} }
} }
@ -183,7 +183,7 @@ class SyncYomiSyncService(
body = body, body = body,
) )
client.newCall(uploadRequest).execute().use { client.newCall(uploadRequest).await().use {
if (it.isSuccessful) { if (it.isSuccessful) {
logcat( logcat(
LogPriority.DEBUG, LogPriority.DEBUG,