sync oubox
This commit is contained in:
@@ -48,6 +48,38 @@ interface FileDao {
|
|||||||
@Query("UPDATE files SET \"exists\" = 0, updated_at = :updatedAt WHERE resource_id = :resourceId")
|
@Query("UPDATE files SET \"exists\" = 0, updated_at = :updatedAt WHERE resource_id = :resourceId")
|
||||||
suspend fun markMissing(resourceId: String, updatedAt: Long)
|
suspend fun markMissing(resourceId: String, updatedAt: Long)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promote le placement après confirmation serveur (`POST /sync/ops` appliqué) :
|
||||||
|
* `local-cloud` si une copie physique existe, sinon `cloud`. Idempotent.
|
||||||
|
*/
|
||||||
|
@Query("""
|
||||||
|
UPDATE files
|
||||||
|
SET sync_status = CASE WHEN uri IS NOT NULL THEN 'local-cloud' ELSE 'cloud' END,
|
||||||
|
updated_at = :now
|
||||||
|
WHERE resource_id = :resourceId
|
||||||
|
""")
|
||||||
|
suspend fun promoteSyncStatus(resourceId: String, now: Long)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill : promu toutes les lignes restées `local` dont une op outbox
|
||||||
|
* `create_resource`/`move_resource` a déjà été `synced` (poussées avant ce
|
||||||
|
* mécanisme). Rattrape le pas pour les données préexistantes.
|
||||||
|
*/
|
||||||
|
@Query("""
|
||||||
|
UPDATE files
|
||||||
|
SET sync_status = CASE WHEN uri IS NOT NULL THEN 'local-cloud' ELSE 'cloud' END,
|
||||||
|
updated_at = :now
|
||||||
|
WHERE sync_status = 'local'
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM pending_operations
|
||||||
|
WHERE pending_operations.resource_id = files.resource_id
|
||||||
|
AND pending_operations.resource_type = 'file'
|
||||||
|
AND pending_operations.status = 'synced'
|
||||||
|
AND pending_operations.operation IN ('create_resource', 'move_resource')
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
suspend fun backfillSyncedStatus(now: Long)
|
||||||
|
|
||||||
@Query("UPDATE files SET folder_resource_id = :folderId, updated_at = :updatedAt WHERE resource_id IN (:resourceIds)")
|
@Query("UPDATE files SET folder_resource_id = :folderId, updated_at = :updatedAt WHERE resource_id IN (:resourceIds)")
|
||||||
suspend fun moveToFolder(resourceIds: List<String>, folderId: String, updatedAt: Long)
|
suspend fun moveToFolder(resourceIds: List<String>, folderId: String, updatedAt: Long)
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,38 @@ interface FolderDao {
|
|||||||
@Query("UPDATE folders SET \"exists\" = 0, updated_at = :updatedAt WHERE resource_id = :resourceId")
|
@Query("UPDATE folders SET \"exists\" = 0, updated_at = :updatedAt WHERE resource_id = :resourceId")
|
||||||
suspend fun markMissing(resourceId: String, updatedAt: Long)
|
suspend fun markMissing(resourceId: String, updatedAt: Long)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promote le placement après confirmation serveur (`POST /sync/ops` appliqué) :
|
||||||
|
* `local-cloud` si une copie physique existe, sinon `cloud`. Idempotent.
|
||||||
|
*/
|
||||||
|
@Query("""
|
||||||
|
UPDATE folders
|
||||||
|
SET sync_status = CASE WHEN uri IS NOT NULL THEN 'local-cloud' ELSE 'cloud' END,
|
||||||
|
updated_at = :now
|
||||||
|
WHERE resource_id = :resourceId
|
||||||
|
""")
|
||||||
|
suspend fun promoteSyncStatus(resourceId: String, now: Long)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill : promu toutes les lignes restées `local` dont une op outbox
|
||||||
|
* `create_resource`/`move_resource` a déjà été `synced` (poussées avant ce
|
||||||
|
* mécanisme). Rattrape le pas pour les données préexistantes.
|
||||||
|
*/
|
||||||
|
@Query("""
|
||||||
|
UPDATE folders
|
||||||
|
SET sync_status = CASE WHEN uri IS NOT NULL THEN 'local-cloud' ELSE 'cloud' END,
|
||||||
|
updated_at = :now
|
||||||
|
WHERE sync_status = 'local'
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM pending_operations
|
||||||
|
WHERE pending_operations.resource_id = folders.resource_id
|
||||||
|
AND pending_operations.resource_type = 'folder'
|
||||||
|
AND pending_operations.status = 'synced'
|
||||||
|
AND pending_operations.operation IN ('create_resource', 'move_resource')
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
suspend fun backfillSyncedStatus(now: Long)
|
||||||
|
|
||||||
@Query("DELETE FROM folders WHERE resource_id = :resourceId")
|
@Query("DELETE FROM folders WHERE resource_id = :resourceId")
|
||||||
suspend fun remove(resourceId: String)
|
suspend fun remove(resourceId: String)
|
||||||
}
|
}
|
||||||
+5
-2
@@ -160,7 +160,7 @@ class FileRepository @Inject constructor(
|
|||||||
return FileEntity(
|
return FileEntity(
|
||||||
id = existing?.id ?: 0L,
|
id = existing?.id ?: 0L,
|
||||||
resourceId = dto.id,
|
resourceId = dto.id,
|
||||||
uri = null,
|
uri = existing?.uri,
|
||||||
name = dto.name,
|
name = dto.name,
|
||||||
folderResourceId = dto.folderId ?: folderResourceId,
|
folderResourceId = dto.folderId ?: folderResourceId,
|
||||||
extension = extension,
|
extension = extension,
|
||||||
@@ -170,7 +170,10 @@ class FileRepository @Inject constructor(
|
|||||||
exists = 1,
|
exists = 1,
|
||||||
lastModified = existing?.lastModified,
|
lastModified = existing?.lastModified,
|
||||||
ownerId = existing?.ownerId,
|
ownerId = existing?.ownerId,
|
||||||
syncStatus = existing?.syncStatus ?: FileStatus.CLOUD,
|
// Le snapshot serveur confirme la présence cloud : si une copie
|
||||||
|
// physique existe aussi, placement local-cloud (jamais d'écrasement
|
||||||
|
// de l'uri / de régression du placement).
|
||||||
|
syncStatus = if (existing?.uri != null) FileStatus.LOCAL_CLOUD else FileStatus.CLOUD,
|
||||||
processed = existing?.processed ?: false,
|
processed = existing?.processed ?: false,
|
||||||
addedAt = existing?.addedAt ?: now,
|
addedAt = existing?.addedAt ?: now,
|
||||||
updatedAt = now,
|
updatedAt = now,
|
||||||
|
|||||||
+6
-3
@@ -69,12 +69,15 @@ class FolderRepository @Inject constructor(
|
|||||||
return FolderEntity(
|
return FolderEntity(
|
||||||
id = existing?.id ?: 0L,
|
id = existing?.id ?: 0L,
|
||||||
resourceId = dto.id,
|
resourceId = dto.id,
|
||||||
uri = null,
|
uri = existing?.uri,
|
||||||
name = dto.name,
|
name = dto.name,
|
||||||
exists = null,
|
exists = existing?.exists,
|
||||||
parentResourceId = dto.parentId,
|
parentResourceId = dto.parentId,
|
||||||
ownerId = existing?.ownerId,
|
ownerId = existing?.ownerId,
|
||||||
syncStatus = existing?.syncStatus ?: FolderStatus.CLOUD,
|
// Le snapshot serveur confirme la présence cloud : si une copie
|
||||||
|
// physique existe aussi, placement local-cloud (jamais d'écrasement
|
||||||
|
// de l'uri / de régression du placement).
|
||||||
|
syncStatus = if (existing?.uri != null) FolderStatus.LOCAL_CLOUD else FolderStatus.CLOUD,
|
||||||
addedAt = existing?.addedAt ?: now,
|
addedAt = existing?.addedAt ?: now,
|
||||||
updatedAt = now,
|
updatedAt = now,
|
||||||
)
|
)
|
||||||
|
|||||||
+29
@@ -14,6 +14,8 @@ import com.squareup.moshi.JsonAdapter
|
|||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.squareup.moshi.Types
|
import com.squareup.moshi.Types
|
||||||
import com.vaultdrop.mobile.auth.TokenProvider
|
import com.vaultdrop.mobile.auth.TokenProvider
|
||||||
|
import com.vaultdrop.mobile.data.local.dao.FileDao
|
||||||
|
import com.vaultdrop.mobile.data.local.dao.FolderDao
|
||||||
import com.vaultdrop.mobile.data.local.dao.PendingOperationDao
|
import com.vaultdrop.mobile.data.local.dao.PendingOperationDao
|
||||||
import com.vaultdrop.mobile.data.local.entity.PendingOperationEntity
|
import com.vaultdrop.mobile.data.local.entity.PendingOperationEntity
|
||||||
import com.vaultdrop.mobile.data.local.entity.PendingOperationType
|
import com.vaultdrop.mobile.data.local.entity.PendingOperationType
|
||||||
@@ -50,6 +52,8 @@ class OutboxSyncWorker @AssistedInject constructor(
|
|||||||
@Assisted workerParams: WorkerParameters,
|
@Assisted workerParams: WorkerParameters,
|
||||||
private val apiClient: ApiClient,
|
private val apiClient: ApiClient,
|
||||||
private val pendingOperationDao: PendingOperationDao,
|
private val pendingOperationDao: PendingOperationDao,
|
||||||
|
private val fileDao: FileDao,
|
||||||
|
private val folderDao: FolderDao,
|
||||||
private val tokenProvider: TokenProvider,
|
private val tokenProvider: TokenProvider,
|
||||||
moshi: Moshi,
|
moshi: Moshi,
|
||||||
) : CoroutineWorker(appContext, workerParams) {
|
) : CoroutineWorker(appContext, workerParams) {
|
||||||
@@ -62,6 +66,11 @@ class OutboxSyncWorker @AssistedInject constructor(
|
|||||||
// Mode local : sans compte connecté, rien à pousser.
|
// Mode local : sans compte connecté, rien à pousser.
|
||||||
if (tokenProvider.current == null) return Result.success()
|
if (tokenProvider.current == null) return Result.success()
|
||||||
|
|
||||||
|
// Backfill : promeut les ressources déjà confirmées par le serveur mais
|
||||||
|
// dont le placement était resté `local` (poussées avant ce mécanisme).
|
||||||
|
fileDao.backfillSyncedStatus(System.currentTimeMillis())
|
||||||
|
folderDao.backfillSyncedStatus(System.currentTimeMillis())
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
val pending = pendingOperationDao.selectPending(BATCH_SIZE)
|
val pending = pendingOperationDao.selectPending(BATCH_SIZE)
|
||||||
if (pending.isEmpty()) {
|
if (pending.isEmpty()) {
|
||||||
@@ -95,6 +104,7 @@ class OutboxSyncWorker @AssistedInject constructor(
|
|||||||
// Ops appliquées par le serveur (indices < applied).
|
// Ops appliquées par le serveur (indices < applied).
|
||||||
for (i in 0 until result.applied) {
|
for (i in 0 until result.applied) {
|
||||||
pendingOperationDao.markSynced(pending[i].id, now)
|
pendingOperationDao.markSynced(pending[i].id, now)
|
||||||
|
promotePlacement(pending[i])
|
||||||
Timber.d("outbox synced %s", pending[i].operationId)
|
Timber.d("outbox synced %s", pending[i].operationId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +127,25 @@ class OutboxSyncWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Après confirmation serveur, promeut le placement de la ressource touchée :
|
||||||
|
* `create_resource`/`move_resource` appliqués → `local-cloud` si copie
|
||||||
|
* physique présente, `cloud` sinon. Rappelle le statut d'une ressource déjà
|
||||||
|
* cloud (idempotent), `delete_resource` n'en modifie pas.
|
||||||
|
*/
|
||||||
|
private suspend fun promotePlacement(op: PendingOperationEntity) {
|
||||||
|
val resourceId = op.resourceId ?: return
|
||||||
|
when (op.operation) {
|
||||||
|
PendingOperationType.CREATE_RESOURCE,
|
||||||
|
PendingOperationType.MOVE_RESOURCE -> {
|
||||||
|
when (op.resourceType) {
|
||||||
|
"file" -> fileDao.promoteSyncStatus(resourceId, System.currentTimeMillis())
|
||||||
|
"folder" -> folderDao.promoteSyncStatus(resourceId, System.currentTimeMillis())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun PendingOperationEntity.toSyncOpDto(): SyncOpDto {
|
private fun PendingOperationEntity.toSyncOpDto(): SyncOpDto {
|
||||||
val payloadMap = runCatching { payloadAdapter.fromJson(payload) }.getOrNull()
|
val payloadMap = runCatching { payloadAdapter.fromJson(payload) }.getOrNull()
|
||||||
return SyncOpDto(
|
return SyncOpDto(
|
||||||
|
|||||||
Reference in New Issue
Block a user