From 66c82624763ab75c95fc7e977bf7630b3f69441f Mon Sep 17 00:00:00 2001 From: Li Wei Date: Thu, 12 Nov 2026 14:21:55 +0000 Subject: [PATCH] fix(auto-upload): allow empty files unchanged for one hour Keep deferring recently modified empty files to avoid uploading camera placeholders. Allow intentional empty files once their modification time is at least one hour old, and cover the grace-period boundaries and file transitions. Assisted-by: Codex:GPT-6 Signed-off-by: Li Wei --- .../jobs/autoUpload/AutoUploadWorker.kt | 5 +- .../jobs/autoUpload/EmptyFileUploadPolicy.kt | 22 ++++ .../autoUpload/EmptyFileUploadPolicyTest.kt | 114 ++++++++++++++++++ 3 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/client/jobs/autoUpload/EmptyFileUploadPolicy.kt create mode 100644 app/src/test/java/com/nextcloud/client/jobs/autoUpload/EmptyFileUploadPolicyTest.kt diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt index b2a70f7ed622..86971010cb9a 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt @@ -301,8 +301,9 @@ class AutoUploadWorker( try { // A camera app can register the file with MediaStore before it finished writing it. - if (file.isFile && file.length() == 0L) { - Log_OC.w(TAG, "skipping empty file, still being written: $localPath") + // Allow empty files unmodified for at least one hour so intentional empty files can be uploaded. + if (EmptyFileUploadPolicy.shouldDefer(file)) { + Log_OC.w(TAG, "deferring empty file until unmodified for one hour: $localPath") return@forEachIndexed } diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/EmptyFileUploadPolicy.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/EmptyFileUploadPolicy.kt new file mode 100644 index 000000000000..87189e3e58e6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/EmptyFileUploadPolicy.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.autoUpload + +import java.io.File +import kotlin.time.Duration.Companion.hours + +internal object EmptyFileUploadPolicy { + private val minimumAgeMillis = 1.hours.inWholeMilliseconds + + fun shouldDefer(file: File, nowMillis: Long = System.currentTimeMillis()): Boolean { + if (!file.isFile || file.length() != 0L) return false + + val lastModified = file.lastModified() + return lastModified <= 0L || nowMillis - lastModified < minimumAgeMillis + } +} diff --git a/app/src/test/java/com/nextcloud/client/jobs/autoUpload/EmptyFileUploadPolicyTest.kt b/app/src/test/java/com/nextcloud/client/jobs/autoUpload/EmptyFileUploadPolicyTest.kt new file mode 100644 index 000000000000..d3ecab65e479 --- /dev/null +++ b/app/src/test/java/com/nextcloud/client/jobs/autoUpload/EmptyFileUploadPolicyTest.kt @@ -0,0 +1,114 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.autoUpload + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import java.io.File +import kotlin.time.Duration.Companion.hours + +class EmptyFileUploadPolicyTest { + @get:Rule + val temporaryFolder = TemporaryFolder() + + private val nowMillis = 2.hours.inWholeMilliseconds + private val oneHourMillis = 1.hours.inWholeMilliseconds + + @Test + fun recentEmptyFileIsDeferred() { + val file = emptyFile(nowMillis - oneHourMillis + 1) + + assertTrue(EmptyFileUploadPolicy.shouldDefer(file, nowMillis)) + } + + @Test + fun emptyFileIsAllowedAtExactlyOneHour() { + val file = emptyFile(nowMillis - oneHourMillis) + + assertFalse(EmptyFileUploadPolicy.shouldDefer(file, nowMillis)) + } + + @Test + fun olderEmptyFileIsAllowed() { + val file = emptyFile(nowMillis - oneHourMillis - 1) + + assertFalse(EmptyFileUploadPolicy.shouldDefer(file, nowMillis)) + } + + @Test + fun modifyingAnOldEmptyFileRestartsTheGracePeriod() { + val file = emptyFile(nowMillis - oneHourMillis) + assertFalse(EmptyFileUploadPolicy.shouldDefer(file, nowMillis)) + + assertTrue(file.setLastModified(nowMillis)) + + assertTrue(EmptyFileUploadPolicy.shouldDefer(file, nowMillis)) + } + + @Test + fun recentNonEmptyFileIsAllowed() { + val file = temporaryFolder.newFile().apply { + writeText("content") + assertTrue(setLastModified(nowMillis)) + } + + assertFalse(EmptyFileUploadPolicy.shouldDefer(file, nowMillis)) + } + + @Test + fun deferredFileIsAllowedAsSoonAsItHasContent() { + val file = emptyFile(nowMillis) + assertTrue(EmptyFileUploadPolicy.shouldDefer(file, nowMillis)) + + file.writeText("content") + assertTrue(file.setLastModified(nowMillis)) + + assertFalse(EmptyFileUploadPolicy.shouldDefer(file, nowMillis)) + } + + @Test + fun emptyFileWithFutureModificationTimeIsDeferred() { + val file = emptyFile(nowMillis + oneHourMillis) + + assertTrue(EmptyFileUploadPolicy.shouldDefer(file, nowMillis)) + } + + @Test + fun emptyFileWithUnknownModificationTimeIsDeferred() { + val file = mock { + on { isFile } doReturn true + on { length() } doReturn 0L + on { lastModified() } doReturn 0L + } + + assertTrue(EmptyFileUploadPolicy.shouldDefer(file, nowMillis)) + } + + @Test + fun missingFileIsLeftToExistingErrorHandling() { + val file = File(temporaryFolder.root, "missing.txt") + + assertFalse(EmptyFileUploadPolicy.shouldDefer(file, nowMillis)) + } + + @Test + fun directoryIsNotTreatedAsAnEmptyFile() { + val directory = temporaryFolder.newFolder() + + assertFalse(EmptyFileUploadPolicy.shouldDefer(directory, nowMillis)) + } + + private fun emptyFile(lastModified: Long): File = temporaryFolder.newFile().apply { + assertTrue(setLastModified(lastModified)) + } +}