Skip to content

Commit 1a40920

Browse files
Merge pull request #15759 from nextcloud/fix/auto-upload-state
fix: auto upload db state
2 parents d4e131e + 6de9b48 commit 1a40920

10 files changed

Lines changed: 242 additions & 207 deletions

File tree

app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.nextcloud.client.account.User;
1919
import com.nextcloud.client.account.UserAccountManager;
2020
import com.nextcloud.client.account.UserAccountManagerImpl;
21+
import com.nextcloud.client.database.entity.UploadEntityKt;
2122
import com.nextcloud.test.RandomStringGenerator;
2223
import com.owncloud.android.AbstractIT;
2324
import com.owncloud.android.MainApp;
@@ -108,7 +109,7 @@ public void largeTest() {
108109
OCUpload upload = createUpload(account);
109110

110111
uploads.add(upload);
111-
uploadsStorageManager.storeUpload(upload);
112+
uploadsStorageManager.uploadDao.insertOrReplace(UploadEntityKt.toUploadEntity(upload));
112113
}
113114

114115
OCUpload[] storedUploads = uploadsStorageManager.getAllStoredUploads();
@@ -151,17 +152,14 @@ public void corruptedUpload() {
151152
account.name);
152153

153154
corruptUpload.setLocalPath(null);
154-
155-
uploadsStorageManager.storeUpload(corruptUpload);
156-
155+
uploadsStorageManager.uploadDao.insertOrReplace(UploadEntityKt.toUploadEntity(corruptUpload));
157156
uploadsStorageManager.getAllStoredUploads();
158157
}
159158

160159
@Test
161160
public void getById() {
162161
OCUpload upload = createUpload(account);
163-
long id = uploadsStorageManager.storeUpload(upload);
164-
162+
long id = uploadsStorageManager.uploadDao.insertOrReplace(UploadEntityKt.toUploadEntity(upload));
165163
OCUpload newUpload = uploadsStorageManager.getUploadById(id);
166164

167165
assertNotNull(newUpload);
@@ -178,7 +176,7 @@ public void getByIdNull() {
178176

179177
private void insertUploads(Account account, int rowsToInsert) {
180178
for (int i = 0; i < rowsToInsert; i++) {
181-
uploadsStorageManager.storeUpload(createUpload(account));
179+
uploadsStorageManager.uploadDao.insertOrReplace(UploadEntityKt.toUploadEntity(createUpload(account)));
182180
}
183181
}
184182

app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,24 @@ package com.nextcloud.client.database.dao
99

1010
import androidx.room.Dao
1111
import androidx.room.Query
12+
import com.nextcloud.client.database.entity.FilesystemEntity
1213
import com.owncloud.android.db.ProviderMeta
1314

1415
@Dao
1516
interface FileSystemDao {
16-
1717
@Query(
1818
"""
19-
SELECT ${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH}
19+
SELECT *
2020
FROM ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME}
2121
WHERE ${ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID} = :syncedFolderId
2222
AND ${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD} = 0
2323
AND ${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER} = 0
24+
AND ${ProviderMeta.ProviderTableMeta._ID} > :lastId
25+
ORDER BY ${ProviderMeta.ProviderTableMeta._ID}
26+
LIMIT :limit
2427
"""
2528
)
26-
suspend fun getAutoUploadFiles(syncedFolderId: String): List<String>
29+
suspend fun getAutoUploadFilesEntities(syncedFolderId: String, limit: Int, lastId: Int): List<FilesystemEntity>
2730

2831
@Query(
2932
"""

app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,24 @@ interface UploadDao {
4545
"AND ${ProviderTableMeta.UPLOADS_REMOTE_PATH} = :remotePath"
4646
)
4747
fun deleteByAccountAndRemotePath(accountName: String, remotePath: String)
48+
49+
@Query(
50+
"SELECT * FROM " + ProviderTableMeta.UPLOADS_TABLE_NAME +
51+
" WHERE " + ProviderTableMeta._ID + " = :id AND " +
52+
ProviderTableMeta.UPLOADS_ACCOUNT_NAME + " = :accountName " +
53+
"LIMIT 1"
54+
)
55+
fun getUploadById(id: Long, accountName: String): UploadEntity?
56+
57+
@Insert(onConflict = OnConflictStrategy.Companion.REPLACE)
58+
fun insertOrReplace(entity: UploadEntity): Long
59+
60+
@Query(
61+
"SELECT * FROM " + ProviderTableMeta.UPLOADS_TABLE_NAME +
62+
" WHERE " + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + " = :accountName AND " +
63+
ProviderTableMeta.UPLOADS_LOCAL_PATH + " = :localPath AND " +
64+
ProviderTableMeta.UPLOADS_REMOTE_PATH + " = :remotePath " +
65+
"LIMIT 1"
66+
)
67+
fun getUploadByAccountAndPaths(accountName: String, localPath: String, remotePath: String): UploadEntity?
4868
}

app/src/main/java/com/nextcloud/client/database/entity/UploadEntity.kt

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/*
22
* Nextcloud - Android Client
33
*
4+
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
45
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
56
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
67
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
@@ -32,7 +33,7 @@ data class UploadEntity(
3233
@ColumnInfo(name = ProviderTableMeta.UPLOADS_FILE_SIZE)
3334
val fileSize: Long?,
3435
@ColumnInfo(name = ProviderTableMeta.UPLOADS_STATUS)
35-
var status: Int?,
36+
val status: Int?,
3637
@ColumnInfo(name = ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR)
3738
val localBehaviour: Int?,
3839
@ColumnInfo(name = ProviderTableMeta.UPLOADS_UPLOAD_TIME)
@@ -78,3 +79,33 @@ fun UploadEntity.toOCUpload(capability: OCCapability? = null): OCUpload {
7879

7980
return upload
8081
}
82+
83+
fun OCUpload.toUploadEntity(): UploadEntity {
84+
val id = if (uploadId == -1L) {
85+
// needed for the insert new records to the db so that insert DAO function returns new generated id
86+
null
87+
} else {
88+
uploadId
89+
}
90+
91+
return UploadEntity(
92+
id = id?.toInt(),
93+
localPath = localPath,
94+
remotePath = remotePath,
95+
accountName = accountName,
96+
fileSize = fileSize,
97+
status = uploadStatus?.value,
98+
localBehaviour = localAction,
99+
nameCollisionPolicy = nameCollisionPolicy?.serialize(),
100+
isCreateRemoteFolder = if (isCreateRemoteFolder) 1 else 0,
101+
102+
// uploadEndTimestamp may overflow max int capacity since it is conversion from long to int. coerceAtMost needed
103+
uploadEndTimestamp = uploadEndTimestamp.coerceAtMost(Int.MAX_VALUE.toLong()).toInt(),
104+
lastResult = lastResult?.value,
105+
createdBy = createdBy,
106+
isWifiOnly = if (isUseWifiOnly) 1 else 0,
107+
isWhileChargingOnly = if (isWhileChargingOnly) 1 else 0,
108+
folderUnlockToken = folderUnlockToken,
109+
uploadTime = null
110+
)
111+
}

app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt

Lines changed: 107 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@ import androidx.work.CoroutineWorker
1717
import androidx.work.WorkerParameters
1818
import com.nextcloud.client.account.User
1919
import com.nextcloud.client.account.UserAccountManager
20+
import com.nextcloud.client.database.entity.UploadEntity
21+
import com.nextcloud.client.database.entity.toOCUpload
22+
import com.nextcloud.client.database.entity.toUploadEntity
2023
import com.nextcloud.client.device.PowerManagementService
2124
import com.nextcloud.client.jobs.BackgroundJobManager
2225
import com.nextcloud.client.jobs.upload.FileUploadWorker
2326
import com.nextcloud.client.network.ConnectivityService
2427
import com.nextcloud.client.preferences.SubFolderRule
2528
import com.nextcloud.utils.ForegroundServiceHelper
29+
import com.nextcloud.utils.extensions.updateStatus
2630
import com.owncloud.android.R
2731
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl
2832
import com.owncloud.android.datamodel.FileDataStorageManager
@@ -41,7 +45,6 @@ import com.owncloud.android.ui.notifications.NotificationUtils
4145
import com.owncloud.android.utils.FileStorageUtils
4246
import com.owncloud.android.utils.FilesSyncHelper
4347
import com.owncloud.android.utils.MimeType
44-
import com.owncloud.android.utils.MimeTypeUtil
4548
import kotlinx.coroutines.Dispatchers
4649
import kotlinx.coroutines.withContext
4750
import java.io.File
@@ -235,37 +238,19 @@ class AutoUploadWorker(
235238
private fun getUserOrReturn(syncedFolder: SyncedFolder): User? {
236239
val optionalUser = userAccountManager.getUser(syncedFolder.account)
237240
if (!optionalUser.isPresent) {
238-
Log_OC.w(TAG, "uploadFilesFromFolder skipped user not present")
241+
Log_OC.w(TAG, "user not present")
239242
return null
240243
}
241244
return optionalUser.get()
242245
}
243246

244-
private fun buildPathsAndMimes(
245-
paths: Set<String>,
246-
syncedFolder: SyncedFolder,
247-
dateFormat: SimpleDateFormat
248-
): List<Triple<String, String, String>> {
249-
val lightVersion = context.resources.getBoolean(R.bool.syncedFolder_light)
250-
val currentLocale = context.resources.configuration.locales[0]
251-
252-
return paths.map { path ->
253-
val file = File(path)
254-
val localPath = file.absolutePath
255-
val remotePath =
256-
getRemotePath(file, syncedFolder, dateFormat, lightVersion, context.resources, currentLocale)
257-
val mimeType = MimeTypeUtil.getBestMimeTypeByFilename(localPath)
258-
Triple(localPath, remotePath, mimeType)
259-
}
260-
}
261-
262247
@Suppress("DEPRECATION")
263248
private fun getUploadSettings(syncedFolder: SyncedFolder): Triple<Boolean, Boolean, Int> {
264249
val lightVersion = context.resources.getBoolean(R.bool.syncedFolder_light)
265250
val accountName = syncedFolder.account
266251

267252
return if (lightVersion) {
268-
Log_OC.d(TAG, "uploadFilesFromFolder light version is used")
253+
Log_OC.d(TAG, "light version is used")
269254
val arbitraryDataProvider = ArbitraryDataProviderImpl(context)
270255
val needsCharging = context.resources.getBoolean(R.bool.syncedFolder_light_on_charging)
271256
val needsWifi = arbitraryDataProvider.getBooleanValue(
@@ -277,7 +262,7 @@ class AutoUploadWorker(
277262
Log_OC.d(TAG, "upload action is: $uploadAction")
278263
Triple(needsCharging, needsWifi, uploadAction)
279264
} else {
280-
Log_OC.d(TAG, "getUploadSettings not light version is used")
265+
Log_OC.d(TAG, "not light version is used")
281266
Triple(syncedFolder.isChargingOnly, syncedFolder.isWifiOnly, syncedFolder.uploadAction)
282267
}
283268
}
@@ -286,48 +271,116 @@ class AutoUploadWorker(
286271
private suspend fun uploadFiles(syncedFolder: SyncedFolder) = withContext(Dispatchers.IO) {
287272
val dateFormat = prepareDateFormat()
288273
val user = getUserOrReturn(syncedFolder) ?: return@withContext
289-
val paths = repository.getAutoUploadFiles(syncedFolder)
290-
if (paths.isEmpty()) {
291-
Log_OC.w(TAG, "uploadFiles skipped paths is empty")
292-
return@withContext
293-
}
294-
295-
val pathsAndMimes = buildPathsAndMimes(paths, syncedFolder, dateFormat)
296-
val (needsCharging, needsWifi, uploadAction) = getUploadSettings(syncedFolder)
297-
298274
val ocAccount = OwnCloudAccount(user.toPlatformAccount(), context)
299275
val client = OwnCloudClientManagerFactory.getDefaultSingleton()
300276
.getClientFor(ocAccount, context)
277+
val lightVersion = context.resources.getBoolean(R.bool.syncedFolder_light)
278+
val currentLocale = context.resources.configuration.locales[0]
301279

302-
pathsAndMimes.forEach { (localPath, remotePath, _) ->
303-
try {
304-
Log_OC.d(TAG, "creating oc upload for ${user.accountName}")
305-
val upload = OCUpload(localPath, remotePath, user.accountName).apply {
306-
nameCollisionPolicy = syncedFolder.nameCollisionPolicy
307-
isUseWifiOnly = needsWifi
308-
isWhileChargingOnly = needsCharging
309-
uploadStatus = UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS
310-
createdBy = UploadFileOperation.CREATED_AS_INSTANT_PICTURE
311-
isCreateRemoteFolder = true
312-
localAction = uploadAction
280+
var lastId = 0
281+
while (true) {
282+
val filePathsWithIds = repository.getFilePathsWithIds(syncedFolder, lastId)
283+
284+
if (filePathsWithIds.isEmpty()) {
285+
Log_OC.w(TAG, "no more files to upload at lastId: $lastId")
286+
break
287+
}
288+
Log_OC.d(TAG, "Processing batch: lastId=$lastId, count=${filePathsWithIds.size}")
289+
290+
filePathsWithIds.forEach { (path, id) ->
291+
val file = File(path)
292+
val localPath = file.absolutePath
293+
val remotePath = getRemotePath(
294+
file,
295+
syncedFolder,
296+
dateFormat,
297+
lightVersion,
298+
context.resources,
299+
currentLocale
300+
)
301+
302+
try {
303+
var (uploadEntity, upload) = createEntityAndUpload(user, localPath, remotePath)
304+
try {
305+
// Insert/update to IN_PROGRESS state before starting upload
306+
val generatedId = uploadsStorageManager.uploadDao.insertOrReplace(uploadEntity)
307+
uploadEntity = uploadEntity.copy(id = generatedId.toInt())
308+
upload.uploadId = generatedId
309+
310+
val operation = createUploadFileOperation(upload, user)
311+
Log_OC.d(TAG, "🕒 uploading: $localPath, id: $generatedId")
312+
313+
val result = operation.execute(client)
314+
uploadsStorageManager.updateStatus(uploadEntity, result.isSuccess)
315+
316+
if (result.isSuccess) {
317+
repository.markFileAsUploaded(localPath, syncedFolder)
318+
Log_OC.d(TAG, "✅ upload completed: $localPath")
319+
} else {
320+
Log_OC.e(
321+
TAG,
322+
"❌ upload failed $localPath (${upload.accountName}): ${result.logMessage}"
323+
)
324+
}
325+
} catch (e: Exception) {
326+
uploadsStorageManager.updateStatus(
327+
uploadEntity,
328+
UploadsStorageManager.UploadStatus.UPLOAD_FAILED
329+
)
330+
Log_OC.e(
331+
TAG,
332+
"Exception during upload file, localPath: $localPath, remotePath: $remotePath," +
333+
" exception: $e"
334+
)
335+
}
336+
} catch (e: Exception) {
337+
Log_OC.e(
338+
TAG,
339+
"Exception uploadFiles during creating entity and upload, localPath: $localPath, " +
340+
"remotePath: $remotePath, exception: $e"
341+
)
313342
}
314343

315-
uploadsStorageManager.storeUpload(upload)
344+
// update last id so upload can continue where it left
345+
lastId = id
346+
}
347+
}
348+
}
349+
350+
private fun createEntityAndUpload(user: User, localPath: String, remotePath: String): Pair<UploadEntity, OCUpload> {
351+
val (needsCharging, needsWifi, uploadAction) = getUploadSettings(syncedFolder)
352+
Log_OC.d(TAG, "creating oc upload for ${user.accountName}")
316353

317-
val operation = createUploadFileOperation(upload, user)
318-
Log_OC.d(TAG, "🕒 uploading: $localPath")
354+
// Get or create upload entity
355+
var uploadEntity = uploadsStorageManager.uploadDao.getUploadByAccountAndPaths(
356+
localPath = localPath,
357+
remotePath = remotePath,
358+
accountName = user.accountName
359+
)
319360

320-
val result = operation.execute(client)
321-
if (result.isSuccess) {
322-
repository.markFileAsUploaded(localPath, syncedFolder)
323-
Log_OC.d(TAG, "✅ auto upload completed: $localPath")
324-
} else {
325-
Log_OC.e(TAG, "❌ auto upload failed: $localPath")
326-
}
327-
} catch (e: Exception) {
328-
Log_OC.e(TAG, "Exception uploadFiles, localPath: $localPath, remotePath: $remotePath, exception: $e")
361+
val upload: OCUpload
362+
if (uploadEntity != null) {
363+
// Existing upload - convert and update status
364+
365+
upload = uploadEntity.toOCUpload(null)
366+
upload.uploadStatus = UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS
367+
uploadEntity = upload.toUploadEntity()
368+
} else {
369+
// New upload - create with all settings
370+
371+
upload = OCUpload(localPath, remotePath, user.accountName).apply {
372+
nameCollisionPolicy = syncedFolder.nameCollisionPolicy
373+
isUseWifiOnly = needsWifi
374+
isWhileChargingOnly = needsCharging
375+
uploadStatus = UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS
376+
createdBy = UploadFileOperation.CREATED_AS_INSTANT_PICTURE
377+
isCreateRemoteFolder = true
378+
localAction = uploadAction
329379
}
380+
uploadEntity = upload.toUploadEntity()
330381
}
382+
383+
return uploadEntity to upload
331384
}
332385

333386
private fun createUploadFileOperation(upload: OCUpload, user: User): UploadFileOperation = UploadFileOperation(

0 commit comments

Comments
 (0)