Skip to content

Commit f6280c0

Browse files
committed
Picture Upload: Fix concurrency issues
For #78
1 parent 59cfa6e commit f6280c0

7 files changed

Lines changed: 49 additions & 6 deletions

File tree

opencloudApp/src/main/java/eu/opencloud/android/workers/AutomaticUploadsWorker.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ class AutomaticUploadsWorker(
9090
} catch (illegalArgumentException: IllegalArgumentException) {
9191
Timber.e(illegalArgumentException, "Source path for picture uploads is not valid")
9292
showNotificationToUpdateUri(SyncType.PICTURE_UPLOADS)
93-
return Result.failure()
9493
}
9594
}
9695
cameraUploadsConfiguration.videoUploadsConfiguration?.let { videoUploadsConfiguration ->
@@ -100,7 +99,6 @@ class AutomaticUploadsWorker(
10099
} catch (illegalArgumentException: IllegalArgumentException) {
101100
Timber.e(illegalArgumentException, "Source path for video uploads is not valid")
102101
showNotificationToUpdateUri(SyncType.VIDEO_UPLOADS)
103-
return Result.failure()
104102
}
105103
}
106104
}
@@ -144,6 +142,16 @@ class AutomaticUploadsWorker(
144142
showNotification(syncType, localPicturesDocumentFiles.size)
145143

146144
for (documentFile in localPicturesDocumentFiles) {
145+
// Dedup: if this content URI already has a queued, in-progress, or succeeded transfer,
146+
// skip it. Without this, a worker killed mid-loop (before updateTimestamp) or a
147+
// file whose lastModified changed (e.g. media scanner) would be re-discovered and
148+
// enqueued with a new upload ID — leading to duplicate uploads or 0-byte files
149+
// when two workers race on the same cache path.
150+
val contentUri = documentFile.uri.toString()
151+
if (transferRepository.existsNonFailedTransferForUri(contentUri)) {
152+
Timber.d("Skipping already-tracked file: %s", documentFile.name)
153+
continue
154+
}
147155
val uploadId = storeInUploadsDatabase(
148156
documentFile = documentFile,
149157
uploadPath = folderBackUpConfiguration.uploadPath.plus(File.separator).plus(documentFile.name),
@@ -305,6 +313,7 @@ class AutomaticUploadsWorker(
305313
forceOverwrite = false,
306314
createdBy = createdByWorker,
307315
spaceId = spaceId,
316+
sourcePath = documentFile.uri.toString(),
308317
)
309318

310319
return transferRepository.saveTransfer(ocTransfer)

opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,13 +204,26 @@ class UploadFileFromContentUriWorker(
204204
}
205205
cacheFile.createNewFile()
206206

207+
// openInputStream can return null if the content provider is unavailable or permissions were revoked.
208+
// Failing here avoids silently uploading a 0-byte file.
207209
val inputStream = appContext.contentResolver.openInputStream(contentUri)
210+
if (inputStream == null) {
211+
Timber.e("Failed to open input stream for %s — content provider unavailable or permissions revoked", contentUri)
212+
throw LocalFileNotFoundException()
213+
}
208214
val outputStream = FileOutputStream(cachePath)
209-
outputStream.use { fileOut ->
210-
inputStream?.copyTo(fileOut)
215+
inputStream.use { input ->
216+
outputStream.use { output ->
217+
input.copyTo(output)
218+
}
219+
}
220+
221+
// Guard against a truncated or empty copy (e.g. file deleted mid-read).
222+
if (cacheFile.length() == 0L) {
223+
Timber.e("Cache file is 0 bytes after copy from %s — source may have been deleted mid-read", contentUri)
224+
cacheFile.delete()
225+
throw LocalFileNotFoundException()
211226
}
212-
inputStream?.close()
213-
outputStream.close()
214227

215228
transferRepository.updateTransferSourcePath(uploadIdInStorageManager, contentUri.toString())
216229
transferRepository.updateTransferLocalPath(uploadIdInStorageManager, cachePath)

opencloudData/src/main/java/eu/opencloud/android/data/transfers/datasources/LocalTransferDataSource.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ interface LocalTransferDataSource {
5757
fun getFinishedTransfers(): List<OCTransfer>
5858
fun clearFailedTransfers()
5959
fun clearSuccessfulTransfers()
60+
fun existsNonFailedTransferForUri(uri: String): Boolean
6061

6162
// TUS state management
6263
fun updateTusState(

opencloudData/src/main/java/eu/opencloud/android/data/transfers/datasources/implementation/OCLocalTransferDataSource.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ class OCLocalTransferDataSource(
141141
transferDao.deleteTransfersWithStatus(TransferStatus.TRANSFER_SUCCEEDED.value)
142142
}
143143

144+
override fun existsNonFailedTransferForUri(uri: String): Boolean =
145+
transferDao.existsNonFailedTransferForUri(uri)
146+
144147
// TUS state management
145148
override fun updateTusState(
146149
id: Long,

opencloudData/src/main/java/eu/opencloud/android/data/transfers/db/TransferDao.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ interface TransferDao {
8888
@Query(DELETE_TRANSFERS_WITH_STATUS)
8989
fun deleteTransfersWithStatus(status: Int)
9090

91+
@Query(EXISTS_NON_FAILED_TRANSFER_FOR_URI)
92+
fun existsNonFailedTransferForUri(uri: String): Boolean
93+
9194
companion object {
9295
private const val SELECT_TRANSFER_WITH_ID = """
9396
SELECT *
@@ -177,5 +180,15 @@ interface TransferDao {
177180
FROM $TRANSFERS_TABLE_NAME
178181
WHERE status = :status
179182
"""
183+
184+
/** status 2 = TRANSFER_FAILED, see [TransferStatus] */
185+
private const val EXISTS_NON_FAILED_TRANSFER_FOR_URI = """
186+
SELECT EXISTS(
187+
SELECT 1
188+
FROM $TRANSFERS_TABLE_NAME
189+
WHERE sourcePath = :uri
190+
AND status != 2
191+
)
192+
"""
180193
}
181194
}

opencloudData/src/main/java/eu/opencloud/android/data/transfers/repository/OCTransferRepository.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ class OCTransferRepository(
112112
override fun clearSuccessfulTransfers() =
113113
localTransferDataSource.clearSuccessfulTransfers()
114114

115+
override fun existsNonFailedTransferForUri(uri: String): Boolean =
116+
localTransferDataSource.existsNonFailedTransferForUri(uri)
117+
115118
// TUS state management
116119
override fun updateTusState(
117120
id: Long,

opencloudDomain/src/main/java/eu/opencloud/android/domain/transfers/TransferRepository.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ interface TransferRepository {
5757
fun getFinishedTransfers(): List<OCTransfer>
5858
fun clearFailedTransfers()
5959
fun clearSuccessfulTransfers()
60+
fun existsNonFailedTransferForUri(uri: String): Boolean
6061

6162
// TUS state management
6263
fun updateTusState(

0 commit comments

Comments
 (0)