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")
|
||||
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)")
|
||||
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")
|
||||
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")
|
||||
suspend fun remove(resourceId: String)
|
||||
}
|
||||
+5
-2
@@ -160,7 +160,7 @@ class FileRepository @Inject constructor(
|
||||
return FileEntity(
|
||||
id = existing?.id ?: 0L,
|
||||
resourceId = dto.id,
|
||||
uri = null,
|
||||
uri = existing?.uri,
|
||||
name = dto.name,
|
||||
folderResourceId = dto.folderId ?: folderResourceId,
|
||||
extension = extension,
|
||||
@@ -170,7 +170,10 @@ class FileRepository @Inject constructor(
|
||||
exists = 1,
|
||||
lastModified = existing?.lastModified,
|
||||
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,
|
||||
addedAt = existing?.addedAt ?: now,
|
||||
updatedAt = now,
|
||||
|
||||
+6
-3
@@ -69,12 +69,15 @@ class FolderRepository @Inject constructor(
|
||||
return FolderEntity(
|
||||
id = existing?.id ?: 0L,
|
||||
resourceId = dto.id,
|
||||
uri = null,
|
||||
uri = existing?.uri,
|
||||
name = dto.name,
|
||||
exists = null,
|
||||
exists = existing?.exists,
|
||||
parentResourceId = dto.parentId,
|
||||
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,
|
||||
updatedAt = now,
|
||||
)
|
||||
|
||||
+29
@@ -14,6 +14,8 @@ import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
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.entity.PendingOperationEntity
|
||||
import com.vaultdrop.mobile.data.local.entity.PendingOperationType
|
||||
@@ -50,6 +52,8 @@ class OutboxSyncWorker @AssistedInject constructor(
|
||||
@Assisted workerParams: WorkerParameters,
|
||||
private val apiClient: ApiClient,
|
||||
private val pendingOperationDao: PendingOperationDao,
|
||||
private val fileDao: FileDao,
|
||||
private val folderDao: FolderDao,
|
||||
private val tokenProvider: TokenProvider,
|
||||
moshi: Moshi,
|
||||
) : CoroutineWorker(appContext, workerParams) {
|
||||
@@ -62,6 +66,11 @@ class OutboxSyncWorker @AssistedInject constructor(
|
||||
// Mode local : sans compte connecté, rien à pousser.
|
||||
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) {
|
||||
val pending = pendingOperationDao.selectPending(BATCH_SIZE)
|
||||
if (pending.isEmpty()) {
|
||||
@@ -95,6 +104,7 @@ class OutboxSyncWorker @AssistedInject constructor(
|
||||
// Ops appliquées par le serveur (indices < applied).
|
||||
for (i in 0 until result.applied) {
|
||||
pendingOperationDao.markSynced(pending[i].id, now)
|
||||
promotePlacement(pending[i])
|
||||
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 {
|
||||
val payloadMap = runCatching { payloadAdapter.fromJson(payload) }.getOrNull()
|
||||
return SyncOpDto(
|
||||
|
||||
Reference in New Issue
Block a user