mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Minor fixes to sync protocol
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
|
||||
package org.isoron.uhabits.sync
|
||||
|
||||
import androidx.test.filters.*
|
||||
import com.fasterxml.jackson.databind.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.mock.*
|
||||
@@ -29,6 +30,7 @@ import junit.framework.Assert.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.junit.*
|
||||
|
||||
@MediumTest
|
||||
class RemoteSyncServerTest {
|
||||
|
||||
private val mapper = ObjectMapper()
|
||||
@@ -53,7 +55,7 @@ class RemoteSyncServerTest {
|
||||
|
||||
@Test
|
||||
fun when_get_data_version_succeeds_should_return_version() = runBlocking {
|
||||
server("/ABC/version") {
|
||||
server("/db/ABC/version") {
|
||||
respondWithJson(GetDataVersionResponse(5))
|
||||
}.apply {
|
||||
assertEquals(5, getDataVersion("ABC"))
|
||||
@@ -63,7 +65,7 @@ class RemoteSyncServerTest {
|
||||
|
||||
@Test(expected = ServiceUnavailable::class)
|
||||
fun when_get_data_version_with_server_error_should_raise_exception() = runBlocking {
|
||||
server("/ABC/version") {
|
||||
server("/db/ABC/version") {
|
||||
respondError(HttpStatusCode.InternalServerError)
|
||||
}.apply {
|
||||
getDataVersion("ABC")
|
||||
@@ -73,7 +75,7 @@ class RemoteSyncServerTest {
|
||||
|
||||
@Test(expected = KeyNotFoundException::class)
|
||||
fun when_get_data_version_with_invalid_key_should_raise_exception() = runBlocking {
|
||||
server("/ABC/version") {
|
||||
server("/db/ABC/version") {
|
||||
respondError(HttpStatusCode.NotFound)
|
||||
}.apply {
|
||||
getDataVersion("ABC")
|
||||
@@ -83,7 +85,7 @@ class RemoteSyncServerTest {
|
||||
|
||||
@Test
|
||||
fun when_get_data_succeeds_should_return_data() = runBlocking {
|
||||
server("/ABC") {
|
||||
server("/db/ABC") {
|
||||
respondWithJson(data)
|
||||
}.apply {
|
||||
assertEquals(data, getData("ABC"))
|
||||
@@ -93,7 +95,7 @@ class RemoteSyncServerTest {
|
||||
|
||||
@Test(expected = KeyNotFoundException::class)
|
||||
fun when_get_data_with_invalid_key_should_raise_exception() = runBlocking {
|
||||
server("/ABC") {
|
||||
server("/db/ABC") {
|
||||
respondError(HttpStatusCode.NotFound)
|
||||
}.apply {
|
||||
getData("ABC")
|
||||
@@ -103,7 +105,7 @@ class RemoteSyncServerTest {
|
||||
|
||||
@Test
|
||||
fun when_put_succeeds_should_not_raise_exceptions() = runBlocking {
|
||||
server("/ABC") {
|
||||
server("/db/ABC") {
|
||||
respondOk()
|
||||
}.apply {
|
||||
put("ABC", data)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package org.isoron.uhabits.sync
|
||||
|
||||
import android.util.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.android.*
|
||||
import io.ktor.client.features.*
|
||||
@@ -26,9 +27,6 @@ import io.ktor.client.features.json.*
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
data class RegisterReponse(val key: String)
|
||||
data class GetDataVersionResponse(val version: Long)
|
||||
|
||||
class RemoteSyncServer(
|
||||
private val baseURL: String = "https://sync.loophabits.org",
|
||||
private val httpClient: HttpClient = HttpClient(Android) {
|
||||
@@ -54,7 +52,10 @@ class RemoteSyncServer(
|
||||
} catch (e: ServerResponseException) {
|
||||
throw ServiceUnavailable()
|
||||
} catch (e: ClientRequestException) {
|
||||
throw KeyNotFoundException()
|
||||
Log.w("RemoteSyncServer", "ClientRequestException", e)
|
||||
if(e.message!!.contains("409")) throw EditConflictException()
|
||||
if(e.message!!.contains("404")) throw KeyNotFoundException()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +66,7 @@ class RemoteSyncServer(
|
||||
} catch (e: ServerResponseException) {
|
||||
throw ServiceUnavailable()
|
||||
} catch (e: ClientRequestException) {
|
||||
Log.w("RemoteSyncServer", "ClientRequestException", e)
|
||||
throw KeyNotFoundException()
|
||||
}
|
||||
}
|
||||
@@ -76,6 +78,7 @@ class RemoteSyncServer(
|
||||
} catch(e: ServerResponseException) {
|
||||
throw ServiceUnavailable()
|
||||
} catch (e: ClientRequestException) {
|
||||
Log.w("RemoteSyncServer", "ClientRequestException", e)
|
||||
throw KeyNotFoundException()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,3 +23,7 @@ data class SyncData(
|
||||
val version: Long,
|
||||
val content: String
|
||||
)
|
||||
|
||||
data class RegisterReponse(val key: String)
|
||||
|
||||
data class GetDataVersionResponse(val version: Long)
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.isoron.uhabits.core.tasks.*
|
||||
import org.isoron.uhabits.tasks.*
|
||||
import org.isoron.uhabits.utils.*
|
||||
import java.io.*
|
||||
import java.lang.RuntimeException
|
||||
import javax.inject.*
|
||||
|
||||
@AppScope
|
||||
@@ -43,7 +44,7 @@ class SyncManager @Inject constructor(
|
||||
|
||||
private val server = RemoteSyncServer()
|
||||
private val tmpFile = File.createTempFile("import", "", context.externalCacheDir)
|
||||
private var currVersion = 0L
|
||||
private var currVersion = 1L
|
||||
private var dirty = true
|
||||
|
||||
private lateinit var encryptionKey: EncryptionKey
|
||||
@@ -72,35 +73,53 @@ class SyncManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun push() {
|
||||
if (!dirty) {
|
||||
Log.i("SyncManager", "Database not dirty. Skipping upload.")
|
||||
return
|
||||
private suspend fun push(depth: Int = 0) {
|
||||
if(depth >= 5) {
|
||||
throw RuntimeException()
|
||||
}
|
||||
if (dirty) {
|
||||
Log.i("SyncManager", "Encrypting local database...")
|
||||
val db = DatabaseUtils.getDatabaseFile(context)
|
||||
val encryptedDB = db.encryptToString(encryptionKey)
|
||||
val size = encryptedDB.length / 1024
|
||||
Log.i("SyncManager", "Pushing local database (version $currVersion, $size KB)")
|
||||
try {
|
||||
server.put(preferences.syncKey, SyncData(currVersion, encryptedDB))
|
||||
dirty = false
|
||||
} catch (e: EditConflictException) {
|
||||
Log.i("SyncManager", "Sync conflict detected while pushing.")
|
||||
setCurrentVersion(0)
|
||||
pull()
|
||||
push(depth = depth + 1)
|
||||
}
|
||||
} else {
|
||||
Log.i("SyncManager", "Local database not modified. Skipping push.")
|
||||
}
|
||||
Log.i("SyncManager", "Encrypting database...")
|
||||
val db = DatabaseUtils.getDatabaseFile(context)
|
||||
val encryptedDB = db.encryptToString(encryptionKey)
|
||||
Log.i("SyncManager", "Uploading database (version ${currVersion}, ${encryptedDB.length / 1024} KB)")
|
||||
server.put(preferences.syncKey, SyncData(currVersion, encryptedDB))
|
||||
dirty = false
|
||||
}
|
||||
|
||||
private suspend fun pull() {
|
||||
Log.i("SyncManager", "Fetching database from server...")
|
||||
val data = server.getData(preferences.syncKey)
|
||||
Log.i("SyncManager", "Fetched database (version ${data.version}, ${data.content.length / 1024} KB)")
|
||||
if (data.version == 0L) {
|
||||
Log.i("SyncManager", "Initial upload detected. Marking db as dirty.")
|
||||
dirty = true
|
||||
}
|
||||
if (data.version <= currVersion) {
|
||||
Log.i("SyncManager", "Local version is up-to-date. Skipping merge.")
|
||||
Log.i("SyncManager", "Querying remote database version...")
|
||||
val remoteVersion = server.getDataVersion(syncKey)
|
||||
Log.i("SyncManager", "Remote database has version $remoteVersion")
|
||||
|
||||
if (remoteVersion <= currVersion) {
|
||||
Log.i("SyncManager", "Local database is up-to-date. Skipping merge.")
|
||||
} else {
|
||||
Log.i("SyncManager", "Decrypting and merging with local changes...")
|
||||
Log.i("SyncManager", "Pulling remote database...")
|
||||
val data = server.getData(syncKey)
|
||||
val size = data.content.length / 1024
|
||||
Log.i("SyncManager", "Pulled remote database (version ${data.version}, $size KB)")
|
||||
Log.i("SyncManager", "Decrypting remote database and merging with local changes...")
|
||||
data.content.decryptToFile(encryptionKey, tmpFile)
|
||||
taskRunner.execute(importDataTaskFactory.create(tmpFile) { tmpFile.delete() })
|
||||
dirty = true
|
||||
setCurrentVersion(data.version + 1)
|
||||
}
|
||||
currVersion = data.version + 1
|
||||
}
|
||||
|
||||
private fun setCurrentVersion(v: Long) {
|
||||
currVersion = v
|
||||
Log.i("SyncManager", "Setting local database version to $currVersion")
|
||||
}
|
||||
|
||||
suspend fun onResume() {
|
||||
@@ -118,6 +137,9 @@ class SyncManager @Inject constructor(
|
||||
}
|
||||
|
||||
override fun onCommandExecuted(command: Command?, refreshKey: Long?) {
|
||||
if (!dirty) {
|
||||
setCurrentVersion(currVersion + 1)
|
||||
}
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user