Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import java.io.File
import java.util.Random
import java.util.UUID
import java.util.function.Supplier
import kotlinx.coroutines.runBlocking

class UploadStorageManagerTest : AbstractIT() {
private lateinit var uploadsStorageManager: UploadsStorageManager
Expand Down Expand Up @@ -65,6 +66,29 @@ class UploadStorageManagerTest : AbstractIT() {
)
}

@Test
fun uploadLookupAndFolderCleanupAreIsolatedByAccount() = runBlocking {
val localPath = "/storage/auto-upload-account-isolation.txt"
val remotePath = "/auto-upload-account-isolation.txt"
val first = OCUpload(localPath, remotePath, account.name)
val second = OCUpload(localPath, remotePath, user2.accountName)
val dao = uploadsStorageManager.uploadDao
val firstId = dao.insertOrReplace(first.toUploadEntity())
val secondId = dao.insertOrReplace(second.toUploadEntity())
try {
Assert.assertEquals(firstId, dao.getByRemotePath(remotePath, account.name)?.id?.toLong())
Assert.assertEquals(secondId, dao.getByRemotePath(remotePath, user2.accountName)?.id?.toLong())

dao.deleteByLocalRemotePath(localPath, remotePath, account.name)

Assert.assertNull(dao.getByRemotePath(remotePath, account.name))
Assert.assertEquals(secondId, dao.getByRemotePath(remotePath, user2.accountName)?.id?.toLong())
} finally {
uploadsStorageManager.removeUpload(firstId)
uploadsStorageManager.removeUpload(secondId)
}
}

@Test
fun testDeleteAllUploads() {
// Clean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ import com.owncloud.android.db.ProviderMeta

@Dao
interface FileSystemDao {
@Query(
"""
SELECT filesystem.* FROM ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME} AS filesystem
INNER JOIN ${ProviderMeta.ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME} AS folders
ON filesystem.${ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID}
= folders.${ProviderMeta.ProviderTableMeta._ID}
WHERE folders.${ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT} = :accountName
AND filesystem.${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_REMOTE_PATH} = :remotePath
AND filesystem.${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD} = 1
AND filesystem.${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER} = 0
"""
)
fun getHandledByRemotePath(remotePath: String, accountName: String): List<FilesystemEntity>

@Query(
"""
UPDATE ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ interface UploadDao {

@Query(
"SELECT * FROM ${ProviderTableMeta.UPLOADS_TABLE_NAME} " +
"WHERE ${ProviderTableMeta.UPLOADS_REMOTE_PATH} = :remotePath LIMIT 1"
"WHERE ${ProviderTableMeta.UPLOADS_REMOTE_PATH} = :remotePath " +
"AND ${ProviderTableMeta.UPLOADS_ACCOUNT_NAME} = :accountName LIMIT 1"
)
fun getByRemotePath(remotePath: String): UploadEntity?
fun getByRemotePath(remotePath: String, accountName: String): UploadEntity?

@Query(
"DELETE FROM ${ProviderTableMeta.UPLOADS_TABLE_NAME} " +
Expand All @@ -48,9 +49,10 @@ interface UploadDao {
DELETE FROM ${ProviderTableMeta.UPLOADS_TABLE_NAME}
WHERE ${ProviderTableMeta.UPLOADS_LOCAL_PATH} = :localPath
AND ${ProviderTableMeta.UPLOADS_REMOTE_PATH} = :remotePath
AND ${ProviderTableMeta.UPLOADS_ACCOUNT_NAME} = :accountName
"""
)
suspend fun deleteByLocalRemotePath(localPath: String, remotePath: String)
suspend fun deleteByLocalRemotePath(localPath: String, remotePath: String, accountName: String)

@Query(
"SELECT * FROM " + ProviderTableMeta.UPLOADS_TABLE_NAME +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@ class BackgroundJobFactory @Inject constructor(
autoUploadHelper = AutoUploadHelper(
FileSystemRepository(dao = database.fileSystemDao(), uploadsStorageManager, context)
),
preferences = preferences
preferences = preferences,
clientRepository = clientRepository
)

private fun createAutoUploadRescanWorker(context: Context, params: WorkerParameters): AutoUploadRescanWorker =
Expand Down Expand Up @@ -268,6 +269,7 @@ class BackgroundJobFactory @Inject constructor(
syncedFolderProvider,
context,
uploadFileOperationFactory,
clientRepository,
params
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ import com.nextcloud.client.jobs.upload.FileUploadEventBroadcaster
import com.nextcloud.client.jobs.upload.FileUploadHelper
import com.nextcloud.client.jobs.upload.FileUploadWorker
import com.nextcloud.client.jobs.upload.UploadDelayPolicy
import com.nextcloud.client.jobs.upload.AutoUploadProgressForwarder
import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager
import com.nextcloud.client.network.ConnectivityService
import com.nextcloud.client.preferences.AppPreferences
import com.nextcloud.repository.ClientRepository
import com.nextcloud.utils.extensions.getLog
import com.nextcloud.utils.extensions.isConflict
import com.nextcloud.utils.extensions.isNonRetryable
Expand All @@ -38,13 +40,12 @@ import com.owncloud.android.datamodel.UploadsStorageManager
import com.owncloud.android.db.OCUpload
import com.owncloud.android.db.UploadResult
import com.owncloud.android.files.services.NameCollisionPolicy
import com.owncloud.android.lib.common.OwnCloudAccount
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.lib.resources.status.OCCapability
import com.owncloud.android.operations.UploadFileOperation
import com.owncloud.android.operations.upload.UploadFileException
import com.owncloud.android.ui.activity.SettingsActivity
import com.owncloud.android.utils.theme.CapabilityUtils
import com.owncloud.android.utils.theme.ViewThemeUtils
Expand All @@ -69,7 +70,8 @@ class AutoUploadWorker(
val viewThemeUtils: ViewThemeUtils,
localBroadcastManager: LocalBroadcastManager,
private val autoUploadHelper: AutoUploadHelper,
private val preferences: AppPreferences
private val preferences: AppPreferences,
private val clientRepository: ClientRepository
) : CoroutineWorker(context, params) {

companion object {
Expand Down Expand Up @@ -115,7 +117,7 @@ class AutoUploadWorker(
return Result.success()
}

uploadFiles(syncedFolder)
if (!uploadFiles(syncedFolder)) return Result.retry()

// only update last scan time after uploading files
syncedFolder.lastScanTimestampMs = System.currentTimeMillis()
Expand Down Expand Up @@ -265,12 +267,11 @@ class AutoUploadWorker(
}

@Suppress("LongMethod", "DEPRECATION", "CyclomaticComplexMethod")
private suspend fun uploadFiles(syncedFolder: SyncedFolder) = withContext(Dispatchers.IO) {
val user = getUserOrReturn(syncedFolder) ?: return@withContext
val ocAccount = OwnCloudAccount(user.toPlatformAccount(), context)
val client = OwnCloudClientManagerFactory.getDefaultSingleton()
.getClientFor(ocAccount, context)
private suspend fun uploadFiles(syncedFolder: SyncedFolder): Boolean = withContext(Dispatchers.IO) {
val user = getUserOrReturn(syncedFolder) ?: return@withContext false
val client = clientRepository.getOwncloudClient(user) ?: return@withContext false
val capability = CapabilityUtils.getCapability(user, context)
var completed = true

updateNotification()

Expand All @@ -290,7 +291,7 @@ class AutoUploadWorker(

if (preferences.isGlobalUploadPaused) {
Log_OC.w(TAG, "⏸️ uploads paused, stopping auto upload")
return@withContext
return@withContext false
}

delay(retryPolicy.getDelay().milliseconds)
Expand Down Expand Up @@ -334,12 +335,15 @@ class AutoUploadWorker(
Log_OC.d(TAG, "🕒 uploading: $localPath, id: $generatedId")

FileUploadWorker.registerActiveUpload(operation)
val progress = AutoUploadProgressForwarder(operation)
operation.addDataTransferProgressListener(progress)
val result = try {
fileUploadEventBroadcaster.sendUploadStarted(operation, context)
operation.execute(client)
} finally {
operation.removeDataTransferProgressListener(progress)
FileUploadWorker.unregisterActiveUpload(operation.ocUploadId)
}
fileUploadEventBroadcaster.sendUploadStarted(operation, context)

UploadErrorNotificationManager.handleResult(
context,
Expand All @@ -352,37 +356,49 @@ class AutoUploadWorker(
)

if (result.isSuccess) {
if (upload.remotePath != operation.remotePath) {
upload.remotePath = operation.remotePath
repository.updateRemotePath(upload, syncedFolder)
}
repository.markFileAsHandled(localPath, syncedFolder)
Log_OC.d(TAG, "✅ upload completed: $localPath")
} else {
completed = false
Log_OC.e(
TAG,
"❌ upload failed $localPath (${upload.accountName}): ${result.logMessage}"
)

if (result.code == ResultCode.UNAUTHORIZED) {
Log_OC.e(TAG, "🔑 credentials are no longer valid, stopping auto upload")
return@withContext
return@withContext false
}

if (result.code == ResultCode.QUOTA_EXCEEDED) {
Log_OC.w(TAG, "quota exceeded, stopping auto upload")
sendUploadFinishEvent(operation, result)
return@withContext
return@withContext false
}

// Mark CONFLICT files as handled to prevent retries
if (result.code.isConflict()) {
repository.markFileAsHandled(localPath, syncedFolder)
Log_OC.w(TAG, "Marked CONFLICT file as handled: $localPath")
}

if (result.exception is UploadFileException.EncryptionKeysUnavailable) {
sendUploadFinishEvent(operation, result)
return@withContext false
}
}

val isLastInBatch = (batchIndex == filePathsWithIds.size - 1)
if (isLastInBatch) {
sendUploadFinishEvent(operation, result)
}
} catch (e: Exception) {
if (e is CancellationException) throw e
completed = false
uploadsStorageManager.updateStatus(
uploadEntity,
UploadsStorageManager.UploadStatus.UPLOAD_FAILED
Expand All @@ -400,6 +416,8 @@ class AutoUploadWorker(
}
}
} catch (e: Exception) {
if (e is CancellationException) throw e
completed = false
Log_OC.e(
TAG,
"Exception uploadFiles during creating entity and upload, localPath: $localPath, " +
Expand All @@ -411,6 +429,7 @@ class AutoUploadWorker(
}
}
}
completed
}

private suspend fun deleteNonExistingFile(path: String, id: Int, upload: OCUpload) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.utils.FileStorageUtils
import com.owncloud.android.utils.MimeType
import com.owncloud.android.utils.theme.CapabilityUtils
import com.nextcloud.client.account.UserAccountManagerImpl
import com.owncloud.android.lib.resources.status.OCCapability
import java.io.File
import java.text.ParsePosition
import java.text.SimpleDateFormat
Expand Down Expand Up @@ -66,7 +68,8 @@ class SyncFolderHelper(private val context: Context) {
subFolderRule
)

val capability = CapabilityUtils.getCapability(context)
val capability = UserAccountManagerImpl.fromContext(context).getUser(syncedFolder.account)
.map { CapabilityUtils.getCapability(it, context) }.orElseGet { OCCapability() }
result = AutoRename.rename(result, capability)

Log_OC.d(TAG, "auto upload remote path: $result")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.jobs.download

import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile

object DownloadEventFileResolver {
fun resolve(storage: FileDataStorageManager, accountName: String?, remotePath: String?): OCFile? {
if (accountName == null || accountName != storage.user.accountName || remotePath.isNullOrEmpty()) return null
return storage.getFileByEncryptedRemotePath(remotePath)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ class FileDownloadWorker(
var file: OCFile? = currentDownload?.file?.fileId?.let { currentUserFileStorageManager?.getFileById(it) }

if (file == null) {
file = currentUserFileStorageManager?.getFileByDecryptedRemotePath(currentDownload?.file?.remotePath)
file = currentUserFileStorageManager?.getFileByEncryptedRemotePath(currentDownload?.file?.remotePath)
}

if (file == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.jobs.upload

import com.owncloud.android.lib.common.network.OnDatatransferProgressListener
import com.owncloud.android.operations.UploadFileOperation

class AutoUploadProgressForwarder(private val operation: UploadFileOperation) : OnDatatransferProgressListener {
override fun onTransferProgress(rate: Long, transferred: Long, total: Long, filename: String?) {
val key = FileUploadHelper.buildRemoteName(operation.user.accountName, operation.uploadRemotePath)
FileUploadHelper.mBoundListeners[key]?.onTransferProgress(rate, transferred, total, operation.fileName)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class FileUploadBroadcastReceiver : BroadcastReceiver() {
FileUploadWorker.cancelUpload(remotePath, accountName)

if (remove) {
removeFileIfAlreadyUploaded(context, remotePath)
removeFileIfAlreadyUploaded(context, uploadId, accountName)
uploadsStorageManager.removeUpload(uploadId)
} else {
FileUploadHelper.instance().updateUploadStatus(
Expand All @@ -79,11 +79,12 @@ class FileUploadBroadcastReceiver : BroadcastReceiver() {
}

@Suppress("DEPRECATION")
private fun removeFileIfAlreadyUploaded(context: Context, remotePath: String) {
private fun removeFileIfAlreadyUploaded(context: Context, uploadId: Long, accountName: String) {
val upload = uploadsStorageManager.getUploadById(uploadId)?.takeIf { it.accountName == accountName } ?: return
val userAccountManager = UserAccountManagerImpl.fromContext(context)
val user = userAccountManager.user
val user = userAccountManager.getUser(accountName).orElse(null) ?: return
val storageManager = FileDataStorageManager(user, context.contentResolver)
val ocFile = storageManager.getFileByPath(remotePath) ?: return
val ocFile = storageManager.getFileByEncryptedRemotePath(upload.remotePath) ?: return
storageManager.removeFile(ocFile, true, false)
}
}
Loading