diff --git a/app/src/main/kotlin/com/syncflow/domain/sync/SyncEngine.kt b/app/src/main/kotlin/com/syncflow/domain/sync/SyncEngine.kt index 5379bb6..3f90d99 100644 --- a/app/src/main/kotlin/com/syncflow/domain/sync/SyncEngine.kt +++ b/app/src/main/kotlin/com/syncflow/domain/sync/SyncEngine.kt @@ -68,6 +68,36 @@ class SyncEngine @Inject constructor( else LocalAccessor.JavaFile(File(localPath)) + /** + * Recursively collect every FILE under [basePath] on the remote, descending into each + * subdirectory (one Depth:1 PROPFIND per directory). Directories are never returned as + * files — only their contents. The provider already drops the parent entry from each + * listing, so children-only is returned; the explicit self-path guard prevents any + * pathological infinite recursion. This MUST mirror the recursive local walk: otherwise + * files in remote subfolders appear absent and a TWO_WAY/MIRROR sync deletes them locally. + */ + private suspend fun listRemoteFilesRecursive( + provider: CloudProvider, + basePath: String, + depth: Int = 0, + ): List { + if (depth > 64) { + Timber.w("SyncEngine: remote recursion depth limit hit at $basePath") + return emptyList() + } + val out = mutableListOf() + for (entry in provider.listFiles(basePath).getOrThrow()) { + if (entry.isDirectory) { + if (entry.path.trimEnd('/') != basePath.trimEnd('/')) { + out += listRemoteFilesRecursive(provider, entry.path, depth + 1) + } + } else { + out += entry + } + } + return out + } + private suspend fun performSync( pair: SyncPair, provider: CloudProvider, @@ -76,8 +106,11 @@ class SyncEngine @Inject constructor( ): SyncResult { val accessor = makeAccessor(pair.localPath) var knownStates = fileStateDao.getForPair(pair.id).associateBy { it.relativePath } - val remoteFiles = provider.listFiles(pair.remotePath).getOrThrow() - .filter { !it.isDirectory } // skip remote directories — they are not sync targets + // The local walk is RECURSIVE, so the remote listing must be too. Listing only the top + // level (Depth:1) made every file inside a remote subfolder look "missing from remote", + // which on a TWO_WAY/MIRROR pair triggered DELETE_LOCAL and wiped those files off the + // device (data loss). Walk the remote tree so subfolder files are matched, not deleted. + val remoteFiles = listRemoteFilesRecursive(provider, pair.remotePath) .associateBy { it.path.removePrefix(pair.remotePath).trimStart('/') } val localFiles = accessor.walkFiles(pair) diff --git a/version.properties b/version.properties index 919ad90..e0c9d89 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=1.0.67 -VERSION_CODE=68 +VERSION_NAME=1.0.68 +VERSION_CODE=69