v1.0.74: exclude OS-volatile junk (.thumbnails/.trashed-/.pending-/.sfpart) symmetrically
Build & Release APK / build (push) Successful in 12m50s
Build & Release APK / build (push) Successful in 12m50s
The .thumbnails cache was synced then DELETE_REMOTE'd every cycle: exclude patterns were applied only to the local walk (filename-only), never to the remote listing, so a previously-uploaded thumbnail looked local-missing and got mirror-deleted endlessly as Android regenerates the cache. - isExcludedPath(): path-segment-aware, hardcoded always-ignored set protects all existing pairs without a DB migration - applied symmetrically to remote listing + merged path set (never upload, download, or delete an excluded path on either side) - add .thumbnails to new-pair default excludePatterns - ExcludePathTest covers cache/trash/pending/sfpart + user patterns
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package com.syncflow.domain.sync
|
||||
|
||||
import com.syncflow.domain.model.ConflictStrategy
|
||||
import com.syncflow.domain.model.DeleteBehavior
|
||||
import com.syncflow.domain.model.ScheduleType
|
||||
import com.syncflow.domain.model.SyncDirection
|
||||
import com.syncflow.domain.model.SyncPair
|
||||
import com.syncflow.domain.model.SyncStatus
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Exclusion symmetry: paths excluded from the local walk must also be excluded from the remote
|
||||
* listing and the merged work set, so a previously-uploaded excluded file is never DELETE_REMOTE'd
|
||||
* in an endless churn loop (the ".thumbnails" cache regression).
|
||||
*/
|
||||
class ExcludePathTest {
|
||||
|
||||
private fun pair(
|
||||
excludePatterns: List<String> = emptyList(),
|
||||
excludeExtensions: List<String> = emptyList(),
|
||||
skipHiddenFiles: Boolean = false,
|
||||
) = SyncPair(
|
||||
id = 1, name = "t", localPath = "/l", remotePath = "/r", accountId = 1,
|
||||
syncDirection = SyncDirection.TWO_WAY,
|
||||
conflictStrategy = ConflictStrategy.KEEP_NEWEST,
|
||||
deleteBehavior = DeleteBehavior.MIRROR,
|
||||
recursive = true,
|
||||
scheduleType = ScheduleType.MANUAL, scheduleIntervalMinutes = 0,
|
||||
scheduleDailyTime = null, scheduleWeekdays = 0,
|
||||
wifiOnly = false, wifiSsid = "", chargingOnly = false, minBatteryPct = 0,
|
||||
excludePatterns = excludePatterns, includeExtensions = emptyList(),
|
||||
excludeExtensions = excludeExtensions, skipHiddenFiles = skipHiddenFiles,
|
||||
minFileSizeKb = 0, maxFileSizeKb = 0,
|
||||
notifyOnComplete = false, notifyOnError = false,
|
||||
isEnabled = true, lastSyncAt = null, lastSyncResult = SyncStatus.IDLE,
|
||||
pendingConflicts = 0,
|
||||
)
|
||||
|
||||
@Test fun `thumbnails cache is always excluded regardless of config`() {
|
||||
val p = pair()
|
||||
assertTrue(isExcludedPath("DCIM/.thumbnails/123.jpg", p))
|
||||
assertTrue(isExcludedPath("Pictures/.thumbnails/1000020397.jpg", p))
|
||||
assertTrue(isExcludedPath(".thumbnails/x.jpg", p))
|
||||
}
|
||||
|
||||
@Test fun `android trash and pending staging dirs are excluded`() {
|
||||
val p = pair()
|
||||
assertTrue(isExcludedPath("DCIM/Camera/.trashed-1700000000-IMG_0001.jpg", p))
|
||||
assertTrue(isExcludedPath("DCIM/.pending-1700000000-VID_0001.mp4", p))
|
||||
}
|
||||
|
||||
@Test fun `our own atomic-write temp files are excluded`() {
|
||||
val p = pair()
|
||||
assertTrue(isExcludedPath("Download/.movie.mp4.sfpart", p))
|
||||
assertTrue(isExcludedPath("a/b/.photo.jpg.sfpart", p))
|
||||
}
|
||||
|
||||
@Test fun `normal media files are not excluded`() {
|
||||
val p = pair()
|
||||
assertFalse(isExcludedPath("DCIM/Camera/IMG_0001.jpg", p))
|
||||
assertFalse(isExcludedPath("Pictures/cat.png", p))
|
||||
assertFalse(isExcludedPath("اصفهان/20171023.jpg", p)) // unicode folder, real data
|
||||
}
|
||||
|
||||
@Test fun `user exclude patterns and extensions still apply on filename`() {
|
||||
assertTrue(isExcludedPath("a/Thumbs.db", pair(excludePatterns = listOf("Thumbs.db"))))
|
||||
assertTrue(isExcludedPath("a/note.tmp", pair(excludePatterns = listOf("*.tmp"))))
|
||||
assertTrue(isExcludedPath("a/data.log", pair(excludeExtensions = listOf("log"))))
|
||||
assertFalse(isExcludedPath("a/keep.jpg", pair(excludeExtensions = listOf("log"))))
|
||||
}
|
||||
|
||||
@Test fun `skipHiddenFiles excludes dotfiles by filename only`() {
|
||||
assertTrue(isExcludedPath("a/.hidden", pair(skipHiddenFiles = true)))
|
||||
assertFalse(isExcludedPath("a/.hidden", pair(skipHiddenFiles = false)))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user