Compare commits

...

2 Commits

Author SHA1 Message Date
amir 537808ca10 v1.0.70: single-source version (name always tracks build number)
Build & Release APK / build (push) Successful in 12m58s
versionName is now derived as 1.0.<versionCode>, so the git tag, APK filename,
and in-app About version are always the same number and can't drift.
2026-06-07 02:08:10 +00:00
amir 147da702a1 v1.0.68: fix two-way DATA LOSS — list remote recursively
Build & Release APK / build (push) Successful in 12m54s
The remote was listed Depth:1 (top level only) while the local folder is
walked recursively. Files inside remote subfolders looked 'missing from
remote', so TWO_WAY + mirror-delete ran DELETE_LOCAL and wiped them off the
device. Now walk the remote tree (Depth:1 per dir) so subfolder files are
matched and never falsely deleted.
2026-06-07 00:43:16 +00:00
3 changed files with 41 additions and 5 deletions
+4 -1
View File
@@ -33,7 +33,10 @@ android {
minSdk = 26
targetSdk = 35
versionCode = versionProps["VERSION_CODE"].toString().toInt()
versionName = versionProps["VERSION_NAME"].toString()
// Single source of truth: the human version always tracks the build number, so the
// git tag (v1.0.N), the APK filename, and the in-app "About" all read 1.0.N and
// can never drift apart again. Bump only VERSION_CODE in version.properties.
versionName = "1.0.${versionProps["VERSION_CODE"].toString().toInt()}"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// Placeholder — replace with real keys before release
@@ -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,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)
+2 -2
View File
@@ -1,2 +1,2 @@
VERSION_NAME=1.0.67
VERSION_CODE=68
VERSION_NAME=1.0.70
VERSION_CODE=70