merge feature
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
package com.vaultdrop.mobile.features.saf
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.room.withTransaction
|
||||
import com.vaultdrop.mobile.data.local.AppDatabase
|
||||
import com.vaultdrop.mobile.data.local.entity.FileEntity
|
||||
import com.vaultdrop.mobile.data.local.entity.FolderEntity
|
||||
import com.vaultdrop.mobile.data.repository.FileRepository
|
||||
import com.vaultdrop.mobile.data.repository.FolderRepository
|
||||
import com.vaultdrop.mobile.data.repository.OutboxRepository
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Suppression de dossiers en mode multi-sélection.
|
||||
*
|
||||
* Un dossier SAF ne peut être supprimé physiquement que vide : on supprime donc
|
||||
* récursivement son contenu (fichiers puis sous-dossiers, du plus profond au
|
||||
* plus proche) avant le dossier lui-même. Room sert de source de vérité pour
|
||||
* l'énumération du contenu (issue du walk SAF).
|
||||
*
|
||||
* Trois modes (partagés avec `FileDeleter`) :
|
||||
* - **LOCALLY** : suppression physique SAF de l'arbre + marquage `exists = 0`
|
||||
* Room, sans outbox (le serveur n'est pas affecté).
|
||||
* - **IN_CLOUD** : enqueue `delete_resource` pour les dossiers uniquement.
|
||||
* - **FULL** : les deux — suppression physique + outbox, transaction atomique.
|
||||
*/
|
||||
@Singleton
|
||||
class FolderDeleter @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val folderRepository: FolderRepository,
|
||||
private val fileRepository: FileRepository,
|
||||
private val outboxRepository: OutboxRepository,
|
||||
private val appDatabase: AppDatabase,
|
||||
) {
|
||||
|
||||
private val resolver: ContentResolver get() = context.contentResolver
|
||||
|
||||
/** Descendants collectés d'un dossier — fichiers à tout niveau + sous-dossiers. */
|
||||
private data class FolderTree(
|
||||
val files: List<FileEntity>,
|
||||
val folders: List<FolderEntity>,
|
||||
)
|
||||
|
||||
suspend fun deleteFolders(
|
||||
folders: List<FolderEntity>,
|
||||
mode: FileDeleter.DeleteMode,
|
||||
): FileDeleter.DeleteReport {
|
||||
var succeeded = 0
|
||||
var failed = 0
|
||||
withContext(Dispatchers.IO) {
|
||||
for (folder in folders) {
|
||||
val ok = when (mode) {
|
||||
FileDeleter.DeleteMode.LOCALLY -> deleteLocally(folder)
|
||||
FileDeleter.DeleteMode.IN_CLOUD -> deleteInCloud(folder)
|
||||
FileDeleter.DeleteMode.FULL -> deleteFull(folder)
|
||||
}
|
||||
if (ok) succeeded++ else failed++
|
||||
}
|
||||
}
|
||||
return FileDeleter.DeleteReport(succeeded, failed)
|
||||
}
|
||||
|
||||
/** Suppression physique locale uniquement (SAF + Room), outbox absente. */
|
||||
private suspend fun deleteLocally(folder: FolderEntity): Boolean {
|
||||
val uri = folder.uri ?: return false
|
||||
val tree = collectDescendants(folder.resourceId)
|
||||
if (deleteTreePhysically(folder, tree)) {
|
||||
val now = System.currentTimeMillis()
|
||||
tree.files.forEach { fileRepository.markMissing(it.resourceId, now) }
|
||||
tree.folders.forEach { folderRepository.markMissing(it.resourceId, now) }
|
||||
folderRepository.markMissing(folder.resourceId, now)
|
||||
Timber.d("deleted folder locally %s", uri)
|
||||
return true
|
||||
}
|
||||
Timber.w("local delete returned false for %s", uri)
|
||||
return false
|
||||
}
|
||||
|
||||
/** Suppression cloud uniquement (outbox `delete_resource`), physique intact. */
|
||||
private suspend fun deleteInCloud(folder: FolderEntity): Boolean {
|
||||
var ok = false
|
||||
runCatching {
|
||||
if (outboxRepository.hasCreateOperationAnyType(folder.resourceId)) {
|
||||
outboxRepository.enqueueDeleteResource(folder.resourceId, "folder")
|
||||
}
|
||||
ok = true
|
||||
Timber.d("enqueued cloud delete for folder %s", folder.resourceId)
|
||||
}.onFailure { e ->
|
||||
Timber.w(e, "enqueueDeleteResource failed for folder %s", folder.resourceId)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
/** Suppression complète : physique + cloud. Transaction atomique Room. */
|
||||
private suspend fun deleteFull(folder: FolderEntity): Boolean {
|
||||
val uri = folder.uri
|
||||
if (uri != null) {
|
||||
val tree = collectDescendants(folder.resourceId)
|
||||
if (deleteTreePhysically(folder, tree)) {
|
||||
val now = System.currentTimeMillis()
|
||||
appDatabase.withTransaction {
|
||||
treeMarkMissing(folder, tree, now)
|
||||
}
|
||||
Timber.d("deleted folder full %s", uri)
|
||||
return true
|
||||
}
|
||||
Timber.w("deleteDocument returned false for %s", uri)
|
||||
return false
|
||||
}
|
||||
// Cloud-only : outbox uniquement.
|
||||
return deleteInCloud(folder)
|
||||
}
|
||||
|
||||
/** Marque missing + outbox `delete_resource` pour toute la descendance, atomique. */
|
||||
private suspend fun treeMarkMissing(folder: FolderEntity, tree: FolderTree, now: Long) {
|
||||
for (file in tree.files) {
|
||||
fileRepository.markMissing(file.resourceId, now)
|
||||
if (outboxRepository.hasCreateOperation(file.resourceId)) {
|
||||
outboxRepository.enqueueDeleteResource(file.resourceId, "file")
|
||||
}
|
||||
}
|
||||
for (desc in tree.folders) {
|
||||
folderRepository.markMissing(desc.resourceId, now)
|
||||
if (outboxRepository.hasCreateOperationAnyType(desc.resourceId)) {
|
||||
outboxRepository.enqueueDeleteResource(desc.resourceId, "folder")
|
||||
}
|
||||
}
|
||||
folderRepository.markMissing(folder.resourceId, now)
|
||||
if (outboxRepository.hasCreateOperationAnyType(folder.resourceId)) {
|
||||
outboxRepository.enqueueDeleteResource(folder.resourceId, "folder")
|
||||
}
|
||||
}
|
||||
|
||||
/** Énumère le contenu d'un dossier (fichiers + sous-dossiers, tout niveau) via Room. */
|
||||
private suspend fun collectDescendants(folderResourceId: String): FolderTree {
|
||||
val files = fileRepository.getFilesInFolder(folderResourceId).toMutableList()
|
||||
val folders = mutableListOf<FolderEntity>()
|
||||
val children = folderRepository.getChildren(folderResourceId)
|
||||
for (child in children) {
|
||||
val sub = collectDescendants(child.resourceId)
|
||||
files += sub.files
|
||||
folders += child
|
||||
folders += sub.folders
|
||||
}
|
||||
return FolderTree(files = files, folders = folders)
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppression SAF physique d'un arbre : fichiers d'abord, puis sous-dossiers
|
||||
* du plus profond au plus proche, puis le dossier racine (deleteDocument
|
||||
* exige un conteneur vide). Retourne `true` si tout l'arbre est supprimé.
|
||||
*/
|
||||
private suspend fun deleteTreePhysically(folder: FolderEntity, tree: FolderTree): Boolean {
|
||||
for (file in tree.files) {
|
||||
if (!deleteDocument(file.uri)) return false
|
||||
}
|
||||
val orderedDirs = tree.folders
|
||||
.map { it to depthOf(it.resourceId, folder.resourceId) }
|
||||
.sortedByDescending { it.second }
|
||||
.map { it.first }
|
||||
for (dir in orderedDirs) {
|
||||
if (!deleteDocument(dir.uri)) return false
|
||||
}
|
||||
return deleteDocument(folder.uri)
|
||||
}
|
||||
|
||||
/** Appelle `deleteDocument` SAF ; sans uri = rien à faire (= succès). */
|
||||
private suspend fun deleteDocument(uri: String?): Boolean {
|
||||
if (uri == null) return true
|
||||
return runCatching {
|
||||
DocumentsContract.deleteDocument(resolver, Uri.parse(uri))
|
||||
}.onFailure { e ->
|
||||
Timber.w(e, "deleteDocument failed for %s", uri)
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
|
||||
/** Profondeur d'un descendant par rapport à la racine de l'arbre (itère Room). */
|
||||
private suspend fun depthOf(resourceId: String, rootResourceId: String): Int {
|
||||
var depth = 0
|
||||
var current = folderRepository.getFolder(resourceId)
|
||||
while (current != null && current.parentResourceId != null && current.parentResourceId != rootResourceId) {
|
||||
depth++
|
||||
current = folderRepository.getFolder(current.parentResourceId)
|
||||
}
|
||||
return depth
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.vaultdrop.mobile.features.saf
|
||||
|
||||
import com.vaultdrop.mobile.data.local.entity.FolderEntity
|
||||
import com.vaultdrop.mobile.data.repository.FileRepository
|
||||
import com.vaultdrop.mobile.data.repository.FolderRepository
|
||||
import com.vaultdrop.mobile.data.repository.SaveFolderInput
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Fusion de dossiers en mode multi-sélection.
|
||||
*
|
||||
* 1. Crée un nouveau dossier nommé `newName` au même niveau que les dossiers
|
||||
* sélectionnés (même parent).
|
||||
* 2. Déplace tout le contenu de chaque dossier source (fichiers + sous-dossiers)
|
||||
* vers le dossier fusionné — relocation SAF quand les deux côtés sont
|
||||
* physiques, repli métadonnée seule sinon (`move_resource` outbox quand même).
|
||||
* 3. Supprime les dossiers sources désormais vides (physique + outbox).
|
||||
*
|
||||
* L'ordre outbox est garanti par l'insertion séquentielle : `create_resource`
|
||||
* du dossier fusionné avant les `move_resource`, avant les `delete_resource`.
|
||||
*/
|
||||
@Singleton
|
||||
class FolderMerger @Inject constructor(
|
||||
private val folderRepository: FolderRepository,
|
||||
private val fileRepository: FileRepository,
|
||||
private val fileMover: FileMover,
|
||||
private val folderDeleter: FolderDeleter,
|
||||
private val safFolderCreator: SafFolderCreator,
|
||||
) {
|
||||
|
||||
data class MergeReport(
|
||||
val movedFiles: Int,
|
||||
val deletedFolders: Int,
|
||||
val failed: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* Fusionne `folders` (frères, même parcours d'explorateur) sous un nouveau
|
||||
* dossier `newName`. Retourne un rapport d'exécution.
|
||||
*/
|
||||
suspend fun mergeSelectedFolders(folders: List<FolderEntity>, newName: String): MergeReport {
|
||||
require(folders.size >= 2) { "merge requires at least 2 folders" }
|
||||
|
||||
// 1. Dossier fusionné au même niveau que les sources.
|
||||
val parentId = folders.first().parentResourceId
|
||||
val merged = createMergedFolder(parentId, newName)
|
||||
|
||||
// 2. Déplacement du contenu de chaque dossier source.
|
||||
var movedFiles = 0
|
||||
var stepFailures = 0
|
||||
withContext(Dispatchers.IO) {
|
||||
for (src in folders) {
|
||||
val ok = runCatching {
|
||||
val files = fileRepository.getFilesInFolder(src.resourceId)
|
||||
if (files.isNotEmpty()) {
|
||||
fileMover.moveFiles(files.map { it.resourceId }, merged.resourceId)
|
||||
movedFiles += files.size
|
||||
}
|
||||
val subFolders = folderRepository.getChildren(src.resourceId)
|
||||
if (subFolders.isNotEmpty()) {
|
||||
folderRepository.reparentSubFolders(subFolders.map { it.resourceId }, merged.resourceId)
|
||||
}
|
||||
}.isSuccess
|
||||
if (!ok) stepFailures++
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Suppression des dossiers sources (désormais sans contenu).
|
||||
var deletedFolders = 0
|
||||
var failed = 0
|
||||
withContext(Dispatchers.IO) {
|
||||
for (src in folders) {
|
||||
val report = folderDeleter.deleteFolders(listOf(src), FileDeleter.DeleteMode.FULL)
|
||||
if (report.failed == 0) deletedFolders++ else failed++
|
||||
}
|
||||
}
|
||||
failed += stepFailures
|
||||
Timber.d("merge '%s': %d fichiers déplacés, %d dossiers supprimés, %d échecs",
|
||||
newName, movedFiles, deletedFolders, failed)
|
||||
return MergeReport(movedFiles = movedFiles, deletedFolders = deletedFolders, failed = failed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée (physique si le parent l'est, cloud-only sinon) et enregistre en Room
|
||||
* le dossier fusionné. Retourne l'entité Room. La sauvegarde enqueue le
|
||||
* `create_resource` dans la même transaction.
|
||||
*/
|
||||
private suspend fun createMergedFolder(parentId: String?, newName: String): FolderEntity {
|
||||
val parent = parentId?.let { folderRepository.getFolder(it) }
|
||||
val createdUri = parent?.uri?.let { safFolderCreator.createFolder(it, newName) }?.toString()
|
||||
return folderRepository.saveFolder(
|
||||
input = SaveFolderInput(
|
||||
uri = createdUri,
|
||||
name = newName,
|
||||
exists = true,
|
||||
createdInApp = true,
|
||||
),
|
||||
parentResourceId = parentId,
|
||||
)
|
||||
}
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
package com.vaultdrop.mobile.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.vaultdrop.mobile.data.local.AppDatabase
|
||||
import com.vaultdrop.mobile.data.local.dao.FolderDao
|
||||
import com.vaultdrop.mobile.data.local.dao.PendingOperationDao
|
||||
import com.vaultdrop.mobile.data.local.entity.FolderEntity
|
||||
import com.vaultdrop.mobile.data.local.entity.PendingOperationEntity
|
||||
import com.vaultdrop.mobile.data.local.entity.PendingOperationType
|
||||
import com.vaultdrop.mobile.data.local.entity.PendingOpStatus
|
||||
import com.vaultdrop.mobile.data.remote.ApiClient
|
||||
import com.vaultdrop.mobile.data.remote.ApiService
|
||||
import com.vaultdrop.mobile.data.remote.dto.ApiEnvelope
|
||||
import com.vaultdrop.mobile.data.remote.dto.DeviceRegistrationDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FileDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.FolderDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.LoginRequestDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.LoginResponseDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.ResolvedUserDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.ResourcePermissionDto
|
||||
import com.vaultdrop.mobile.data.remote.dto.SyncOpsRequest
|
||||
import com.vaultdrop.mobile.data.remote.dto.SyncOpsResult
|
||||
import com.vaultdrop.mobile.domain.DeviceIdentity
|
||||
import com.vaultdrop.mobile.domain.GenerateId
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import retrofit2.Response
|
||||
|
||||
/**
|
||||
* Garde-fous data-layer de la fusion de dossiers : le re-parentage repositionne
|
||||
* les sous-dossiers et journalise un `move_resource` par dossier (ordre outbox),
|
||||
* et le garde `delete_resource` reconnaît les dossiers poussés (`resource_type =
|
||||
* 'folder'`) via `hasCreateOperationAnyType`.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class FolderMergeDataTest {
|
||||
|
||||
private lateinit var db: AppDatabase
|
||||
private lateinit var folderDao: FolderDao
|
||||
private lateinit var opsDao: PendingOperationDao
|
||||
private lateinit var folderRepository: FolderRepository
|
||||
private lateinit var outboxRepository: OutboxRepository
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
folderDao = db.folderDao()
|
||||
opsDao = db.pendingOperationDao()
|
||||
|
||||
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
||||
val generateId = GenerateId()
|
||||
val deviceIdentity = DeviceIdentity(db.userPreferenceDao(), generateId)
|
||||
outboxRepository = OutboxRepository(opsDao, generateId, moshi)
|
||||
folderRepository = FolderRepository(
|
||||
folderDao = folderDao,
|
||||
apiClient = ApiClient(FolderMergeApiService(), moshi),
|
||||
generateId = generateId,
|
||||
deviceIdentity = deviceIdentity,
|
||||
appDatabase = db,
|
||||
outboxRepository = outboxRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reparent_repositionne_les_sous_dossiers_et_enqueue_move_resource_par_dossier() = runTest {
|
||||
folderDao.upsert(folder(ROOT, parent = null))
|
||||
folderDao.upsert(folder(CHILD_A, parent = ROOT))
|
||||
folderDao.upsert(folder(CHILD_B, parent = ROOT))
|
||||
|
||||
folderRepository.reparentSubFolders(listOf(CHILD_A, CHILD_B), MERGED)
|
||||
|
||||
assertEquals("parent_A repositionné", MERGED, folderDao.getByResourceId(CHILD_A)!!.parentResourceId)
|
||||
assertEquals("parent_B repositionné", MERGED, folderDao.getByResourceId(CHILD_B)!!.parentResourceId)
|
||||
|
||||
val ops = opsDao.selectPending(10)
|
||||
assertEquals("une op move par dossier (ordre outbox)", 2, ops.size)
|
||||
assertEquals(PendingOperationType.MOVE_RESOURCE, ops[0].operation)
|
||||
assertEquals("folder", ops[0].resourceType)
|
||||
assertEquals(CHILD_A, ops[0].resourceId)
|
||||
assertEquals(ROOT_WITH_MERGED, ops[0].payload)
|
||||
assertEquals(PendingOperationType.MOVE_RESOURCE, ops[1].operation)
|
||||
assertEquals(CHILD_B, ops[1].resourceId)
|
||||
assertEquals(PendingOpStatus.PENDING, ops[1].status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reparent_expose_les_enfants_au_nouveau_parent() = runTest {
|
||||
folderDao.upsert(folder(ROOT, parent = null))
|
||||
folderDao.upsert(folder(CHILD_A, parent = ROOT))
|
||||
folderDao.upsert(folder(CHILD_B, parent = ROOT))
|
||||
// L'ancien parent n'a plus d'enfants visibles.
|
||||
assertEquals(2, folderRepository.getChildren(ROOT).size)
|
||||
|
||||
folderRepository.reparentSubFolders(listOf(CHILD_A, CHILD_B), MERGED)
|
||||
|
||||
assertEquals(listOf(CHILD_A, CHILD_B), folderRepository.getChildren(MERGED).map { it.resourceId })
|
||||
assertEquals("l'ancien parent est vide", 0, folderRepository.getChildren(ROOT).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reparent_vide_est_un_noop() = runTest {
|
||||
folderRepository.reparentSubFolders(emptyList(), MERGED)
|
||||
assertEquals(0, opsDao.selectPending(10).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hasCreateOperationAnyType_reconnait_les_dossiers_pousses() = runTest {
|
||||
opsDao.insert(
|
||||
PendingOperationEntity(
|
||||
operationId = "1".repeat(32),
|
||||
resourceId = FOLDER_ID,
|
||||
resourceType = "folder",
|
||||
operation = PendingOperationType.CREATE_RESOURCE,
|
||||
payload = "{}",
|
||||
status = PendingOpStatus.PENDING,
|
||||
createdAt = NOW,
|
||||
updatedAt = NOW,
|
||||
),
|
||||
)
|
||||
|
||||
assertFalse(
|
||||
"le gate « processed » (fichiers) reste scindé sur resource_type = 'file'",
|
||||
outboxRepository.hasCreateOperation(FOLDER_ID),
|
||||
)
|
||||
assertTrue(
|
||||
"un delete_resource peut être émis pour un dossier déjà poussé",
|
||||
outboxRepository.hasCreateOperationAnyType(FOLDER_ID),
|
||||
)
|
||||
}
|
||||
|
||||
// --- fixtures ------------------------------------------------------------
|
||||
|
||||
private fun folder(resourceId: String, parent: String?) = FolderEntity(
|
||||
resourceId = resourceId,
|
||||
uri = null,
|
||||
name = resourceId,
|
||||
exists = 1,
|
||||
parentResourceId = parent,
|
||||
ownerId = OWNER,
|
||||
syncStatus = "cloud",
|
||||
addedAt = NOW,
|
||||
updatedAt = NOW,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val NOW = 1_700_000_000_000L
|
||||
const val ROOT = "11111111111111111111111111111111"
|
||||
const val CHILD_A = "22222222222222222222222222222222"
|
||||
const val CHILD_B = "33333333333333333333333333333333"
|
||||
const val MERGED = "44444444444444444444444444444444"
|
||||
const val OWNER = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
const val FOLDER_ID = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
val ROOT_WITH_MERGED = """{"toFolderResourceId":"$MERGED"}"""
|
||||
}
|
||||
}
|
||||
|
||||
/** Fake `ApiService` — aucun endpoint n'est consommé par `reparentSubFolders`. */
|
||||
private class FolderMergeApiService : ApiService {
|
||||
override suspend fun listFolders(): Response<ApiEnvelope<List<FolderDto>>> =
|
||||
Response.success(ApiEnvelope(data = emptyList()))
|
||||
|
||||
override suspend fun listFiles(
|
||||
folderId: String?,
|
||||
page: Int?,
|
||||
pageSize: Int?,
|
||||
): Response<ApiEnvelope<List<FileDto>>> = Response.success(ApiEnvelope(data = emptyList()))
|
||||
|
||||
override suspend fun registerDevice(
|
||||
body: DeviceRegistrationDto,
|
||||
): Response<ApiEnvelope<DeviceRegistrationDto>> = Response.success(ApiEnvelope(data = body))
|
||||
|
||||
override suspend fun login(
|
||||
body: LoginRequestDto,
|
||||
): Response<ApiEnvelope<LoginResponseDto>> = Response.success(ApiEnvelope())
|
||||
|
||||
override suspend fun syncOps(
|
||||
body: SyncOpsRequest,
|
||||
): Response<ApiEnvelope<SyncOpsResult>> = Response.success(ApiEnvelope(data = SyncOpsResult(applied = 0)))
|
||||
|
||||
override suspend fun resolveUser(
|
||||
username: String,
|
||||
): Response<ApiEnvelope<ResolvedUserDto>> = Response.success(ApiEnvelope())
|
||||
|
||||
override suspend fun syncPermissions(
|
||||
after: Long?,
|
||||
): Response<ApiEnvelope<List<ResourcePermissionDto>>> =
|
||||
Response.success(ApiEnvelope(data = emptyList()))
|
||||
}
|
||||
Reference in New Issue
Block a user