v1.0.75: exclude Android app-private trees (Android/data,media,obb)
Build & Release APK / build (push) Successful in 12m49s
Build & Release APK / build (push) Successful in 12m49s
Scoped storage (Android 11+) lets a SAF grant LIST another app's Android/media|data|obb dir but not OPEN the files, so every transfer there failed and the pair was stuck reporting Partial forever (65 failures under Android/media/com.whatsapp/ on Zahra). These hold app-managed data, not user content, so exclude the whole subtree on both sides via path-prefix match in isExcludedPath. ExcludePathTest covers the new prefixes + case-insensitivity.
This commit is contained in:
@@ -433,6 +433,15 @@ private val ALWAYS_IGNORED_SEGMENTS = setOf(".thumbnails")
|
|||||||
private val ALWAYS_IGNORED_PREFIXES = listOf(".trashed-", ".pending-")
|
private val ALWAYS_IGNORED_PREFIXES = listOf(".trashed-", ".pending-")
|
||||||
private val ALWAYS_IGNORED_SUFFIXES = listOf(".sfpart")
|
private val ALWAYS_IGNORED_SUFFIXES = listOf(".sfpart")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App-private storage trees. On Android 11+ (scoped storage) another app's SAF grant can LIST
|
||||||
|
* these directories but cannot OPEN the files inside, so every transfer fails and the pair is
|
||||||
|
* stuck reporting "Partial" forever (e.g. Android/media/com.whatsapp/...). They hold app-managed
|
||||||
|
* data, not user content worth syncing, so they are excluded entirely. Matched case-insensitively
|
||||||
|
* against the full relative path so the whole subtree is ignored on both sides.
|
||||||
|
*/
|
||||||
|
private val ALWAYS_IGNORED_PATH_PREFIXES = listOf("android/data/", "android/media/", "android/obb/")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if [rel] should be excluded from sync entirely. Applied symmetrically to the local walk,
|
* True if [rel] should be excluded from sync entirely. Applied symmetrically to the local walk,
|
||||||
* the remote listing, and known state so an excluded path is never uploaded, downloaded, or
|
* the remote listing, and known state so an excluded path is never uploaded, downloaded, or
|
||||||
@@ -441,7 +450,10 @@ private val ALWAYS_IGNORED_SUFFIXES = listOf(".sfpart")
|
|||||||
* existing local-walk semantics in LocalAccessor.
|
* existing local-walk semantics in LocalAccessor.
|
||||||
*/
|
*/
|
||||||
internal fun isExcludedPath(rel: String, pair: SyncPair): Boolean {
|
internal fun isExcludedPath(rel: String, pair: SyncPair): Boolean {
|
||||||
val segments = rel.replace('\\', '/').split('/').filter { it.isNotEmpty() }
|
val normalized = rel.replace('\\', '/')
|
||||||
|
val lower = normalized.lowercase()
|
||||||
|
if (ALWAYS_IGNORED_PATH_PREFIXES.any { lower.startsWith(it) }) return true
|
||||||
|
val segments = normalized.split('/').filter { it.isNotEmpty() }
|
||||||
if (segments.isEmpty()) return false
|
if (segments.isEmpty()) return false
|
||||||
for (seg in segments) {
|
for (seg in segments) {
|
||||||
if (seg in ALWAYS_IGNORED_SEGMENTS) return true
|
if (seg in ALWAYS_IGNORED_SEGMENTS) return true
|
||||||
|
|||||||
@@ -57,6 +57,22 @@ class ExcludePathTest {
|
|||||||
assertTrue(isExcludedPath("a/b/.photo.jpg.sfpart", p))
|
assertTrue(isExcludedPath("a/b/.photo.jpg.sfpart", p))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test fun `android app-private trees are excluded (scoped-storage unreadable)`() {
|
||||||
|
val p = pair()
|
||||||
|
assertTrue(isExcludedPath("Android/media/com.whatsapp/WhatsApp/Media/IMG.jpg", p))
|
||||||
|
assertTrue(isExcludedPath("Android/data/com.foo/files/x.bin", p))
|
||||||
|
assertTrue(isExcludedPath("Android/obb/com.game/main.obb", p))
|
||||||
|
assertTrue(isExcludedPath("android/MEDIA/com.x/y.jpg", p)) // case-insensitive
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `non-private Android paths are not excluded`() {
|
||||||
|
val p = pair()
|
||||||
|
// A user folder literally named "Android" at a deeper level is fine; only the
|
||||||
|
// top-level app-private trees are blocked.
|
||||||
|
assertFalse(isExcludedPath("DCIM/Android/holiday.jpg", p))
|
||||||
|
assertFalse(isExcludedPath("Pictures/android-wallpaper.png", p))
|
||||||
|
}
|
||||||
|
|
||||||
@Test fun `normal media files are not excluded`() {
|
@Test fun `normal media files are not excluded`() {
|
||||||
val p = pair()
|
val p = pair()
|
||||||
assertFalse(isExcludedPath("DCIM/Camera/IMG_0001.jpg", p))
|
assertFalse(isExcludedPath("DCIM/Camera/IMG_0001.jpg", p))
|
||||||
|
|||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
VERSION_NAME=1.0.74
|
VERSION_NAME=1.0.75
|
||||||
VERSION_CODE=74
|
VERSION_CODE=75
|
||||||
|
|||||||
Reference in New Issue
Block a user