|
|
|
@@ -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<RemoteFile> {
|
|
|
|
|
if (depth > 64) {
|
|
|
|
|
Timber.w("SyncEngine: remote recursion depth limit hit at $basePath")
|
|
|
|
|
return emptyList()
|
|
|
|
|
}
|
|
|
|
|
val out = mutableListOf<RemoteFile>()
|
|
|
|
|
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,7 +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()
|
|
|
|
|
// 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)
|
|
|
|
|
|
|
|
|
@@ -93,7 +127,7 @@ class SyncEngine @Inject constructor(
|
|
|
|
|
|
|
|
|
|
val allPaths = (localFiles.keys + remoteFiles.keys + knownStates.keys).toSet()
|
|
|
|
|
val hasPriorSyncState = knownStates.isNotEmpty()
|
|
|
|
|
val semaphore = Semaphore(4)
|
|
|
|
|
val semaphore = Semaphore(2) // limit concurrency to be gentle on the server
|
|
|
|
|
val uploadedAtomic = AtomicInteger(0)
|
|
|
|
|
val downloadedAtomic = AtomicInteger(0)
|
|
|
|
|
val deletedAtomic = AtomicInteger(0)
|
|
|
|
|