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
1,308 changes: 1,308 additions & 0 deletions app/schemas/com.nextcloud.client.database.NextcloudDatabase/99.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@ import com.owncloud.android.db.ProviderMeta
AutoMigration(from = 93, to = 94, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 94, to = 95, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 95, to = 96),
AutoMigration(from = 96, to = 97, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class)
AutoMigration(from = 96, to = 97, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
// manual migration used for 97 to 98
AutoMigration(from = 98, to = 99)
],
exportSchema = true
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,25 @@ import com.owncloud.android.db.ProviderMeta

@Dao
interface FileSystemDao {
@Query(
"""
UPDATE ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME}
SET ${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_REMOTE_PATH} = :remotePath
WHERE ${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH} = :localPath
AND ${ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID} = :syncedFolderId
"""
)
suspend fun updateRemotePath(remotePath: String, localPath: String, syncedFolderId: String)

@Query(
"""
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME}
WHERE ${ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID} = :syncedFolderId
"""
)
suspend fun getBySyncedFolderId(syncedFolderId: String): List<FilesystemEntity>

@Query(
"""
SELECT COUNT(*) > 0 FROM ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,14 @@ interface SyncedFolderDao {

@Query("SELECT * FROM ${ProviderMeta.ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME}")
fun getAllAsFlow(): Flow<List<SyncedFolderEntity>>

@Query(
"""
SELECT * FROM ${ProviderMeta.ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME}
WHERE ${ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_REMOTE_PATH} = :remotePath
AND ${ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT} = :account
LIMIT 1
"""
)
suspend fun findByRemotePathAndAccount(remotePath: String, account: String): SyncedFolderEntity?
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ interface UploadDao {
)
fun deleteByRemotePathAndAccountName(remotePath: String, accountName: String)

@Query(
"""
DELETE FROM ${ProviderTableMeta.UPLOADS_TABLE_NAME}
WHERE ${ProviderTableMeta.UPLOADS_LOCAL_PATH} = :localPath
AND ${ProviderTableMeta.UPLOADS_REMOTE_PATH} = :remotePath
"""
)
suspend fun deleteByLocalRemotePath(localPath: String, remotePath: String)

@Query(
"SELECT * FROM " + ProviderTableMeta.UPLOADS_TABLE_NAME +
" WHERE " + ProviderTableMeta._ID + " = :id AND " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ data class FilesystemEntity(
val id: Int?,
@ColumnInfo(name = ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH)
val localPath: String?,
@ColumnInfo(name = ProviderTableMeta.FILESYSTEM_FILE_REMOTE_PATH)
val remotePath: String?,
@ColumnInfo(name = ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER)
val fileIsFolder: Int?,
@ColumnInfo(name = ProviderTableMeta.FILESYSTEM_FILE_FOUND_RECENTLY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ class AutoUploadWorker(
try {
// Insert/update to IN_PROGRESS state before starting upload
val generatedId = uploadsStorageManager.uploadDao.insertOrReplace(uploadEntity)
repository.updateRemotePath(upload, syncedFolder)
uploadEntity = uploadEntity.copy(id = generatedId.toInt())
upload.uploadId = generatedId

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.nextcloud.client.database.entity.FilesystemEntity
import com.nextcloud.utils.extensions.shouldSkipFile
import com.owncloud.android.datamodel.SyncedFolder
import com.owncloud.android.datamodel.UploadsStorageManager
import com.owncloud.android.db.OCUpload
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.utils.SyncedFolderUtils
import java.io.File
Expand Down Expand Up @@ -80,6 +81,21 @@ class FileSystemRepository(
return filtered
}

suspend fun updateRemotePath(upload: OCUpload, syncedFolder: SyncedFolder) {
val syncedFolderIdStr = syncedFolder.id.toString()

try {
dao.updateRemotePath(remotePath = upload.remotePath, localPath = upload.localPath, syncedFolderIdStr)
Log_OC.d(
TAG,
"file system entity remote path updated. remotePath: ${upload.remotePath}, localPath: " +
"${upload.localPath} for syncedFolderId=$syncedFolderIdStr"
)
} catch (e: Exception) {
Log_OC.e(TAG, "updateRemotePath(): ${e.message}", e)
}
}

suspend fun markFileAsHandled(localPath: String, syncedFolder: SyncedFolder) {
val syncedFolderIdStr = syncedFolder.id.toString()

Expand Down Expand Up @@ -199,6 +215,7 @@ class FileSystemRepository(
val newEntity = FilesystemEntity(
id = entity?.id,
localPath = localPath,
remotePath = null, // will be updated later
fileIsFolder = if (file.isDirectory) 1 else 0,
fileFoundRecently = System.currentTimeMillis(),
fileSentForUpload = 0, // Reset to 0 to queue for upload
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import android.content.Context
import android.content.Intent
import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.database.entity.SyncedFolderEntity
import com.nextcloud.client.database.entity.UploadEntity
import com.nextcloud.client.database.entity.toOCUpload
import com.nextcloud.client.database.entity.toUploadEntity
Expand All @@ -39,6 +40,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation
import com.owncloud.android.lib.resources.files.model.RemoteFile
import com.owncloud.android.lib.resources.files.model.ServerFileInterface
import com.owncloud.android.lib.resources.status.OCCapability
import com.owncloud.android.operations.RemoveFileOperation
import com.owncloud.android.operations.UploadFileOperation
Expand Down Expand Up @@ -615,4 +617,58 @@ class FileUploadHelper {
}
}
}

/**
* When a synced folder is disabled or deleted, its associated OCUpload entries in the uploads
* table must be cleaned up. Without this, stale upload entries outlive the folder config that
* created them, causing FileUploadWorker to keep retrying uploads for a folder that no longer
* exists or is intentionally turned off, and AutoUploadWorker to re-queue already handled files
* on its next scan via FileSystemRepository.getFilePathsWithIds.
*/
suspend fun removeEntityFromUploadEntities(id: Long) {
uploadsStorageManager.fileSystemDao.getBySyncedFolderId(id.toString())
.filter { it.localPath != null && it.remotePath != null }
.forEach {
Log_OC.d(
TAG,
"deleting upload entity localPath: ${it.localPath}, " + "remotePath: ${it.remotePath}"
)
uploadsStorageManager.uploadDao.deleteByLocalRemotePath(
localPath = it.localPath!!,
remotePath = it.remotePath!!
)
}
}

/**
* Splits a list of files into:
* 1. Files that have an auto-upload folder configured.
* 2. Files that don't.
*/
suspend fun splitFilesByAutoUpload(
files: List<OCFile>,
accountName: String
): Pair<List<SyncedFolderEntity>, List<OCFile>> {

val autoUploadFolders = mutableListOf<SyncedFolderEntity>()
val nonAutoUploadFiles = mutableListOf<OCFile>()

for (file in files) {
val entity = getAutoUploadFolderEntity(file, accountName)
if (entity != null) {
autoUploadFolders.add(entity)
} else {
nonAutoUploadFiles.add(file)
}
}

return autoUploadFolders to nonAutoUploadFiles
}

suspend fun getAutoUploadFolderEntity(file: ServerFileInterface, accountName: String): SyncedFolderEntity? {
val dao = uploadsStorageManager.syncedFolderDao
val normalizedRemotePath = file.remotePath.trimEnd()
if (normalizedRemotePath.isEmpty()) return null
return dao.findByRemotePathAndAccount(normalizedRemotePath, accountName)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.utils.extensions

import com.owncloud.android.R
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.ui.activity.FileActivity
import com.owncloud.android.ui.activity.FileDisplayActivity
import com.owncloud.android.ui.activity.OnFilesRemovedListener

fun FileActivity.removeFiles(
offlineFiles: List<OCFile>,
files: List<OCFile>,
onlyLocalCopy: Boolean,
filesRemovedListener: OnFilesRemovedListener?
) {
connectivityService.isNetworkAndServerAvailable { isAvailable ->
if (isAvailable) {
showLoadingDialog(getString(R.string.wait_a_moment))

(this as? FileDisplayActivity)
?.deleteBatchTracker
?.startBatchDelete(files.size)

if (files.isNotEmpty()) {
val inBackground = (files.size != 1)
fileOperationsHelper?.removeFiles(files, onlyLocalCopy, inBackground)
}

if (offlineFiles.isNotEmpty()) {
filesRemovedListener?.onFilesRemoved()
}

dismissLoadingDialog()
} else {
if (onlyLocalCopy) {
fileOperationsHelper?.removeFiles(files, true, true)
} else {
files.forEach(storageManager::addRemoveFileOfflineOperation)
}

filesRemovedListener?.onFilesRemoved()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import com.nextcloud.client.account.CurrentAccountProvider;
import com.nextcloud.client.account.User;
import com.nextcloud.client.database.NextcloudDatabase;
import com.nextcloud.client.database.dao.FileSystemDao;
import com.nextcloud.client.database.dao.SyncedFolderDao;
import com.nextcloud.client.database.dao.UploadDao;
import com.nextcloud.client.database.entity.UploadEntity;
import com.nextcloud.client.database.entity.UploadEntityKt;
Expand Down Expand Up @@ -69,7 +71,9 @@ public class UploadsStorageManager extends Observable {
private final ContentResolver contentResolver;
private final CurrentAccountProvider currentAccountProvider;
private OCCapability capability;
public final UploadDao uploadDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).uploadDao();
public final UploadDao uploadDao = NextcloudDatabase.instance().uploadDao();
public final FileSystemDao fileSystemDao = NextcloudDatabase.instance().fileSystemDao();
public final SyncedFolderDao syncedFolderDao = NextcloudDatabase.instance().syncedFolderDao();

public UploadsStorageManager(
CurrentAccountProvider currentAccountProvider,
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/com/owncloud/android/db/ProviderMeta.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
*/
public class ProviderMeta {
public static final String DB_NAME = "filelist";
public static final int DB_VERSION = 98;
public static final int DB_VERSION = 99;

private ProviderMeta() {
// No instance
Expand Down Expand Up @@ -361,6 +361,7 @@ static public class ProviderTableMeta implements BaseColumns {

// Columns of filesystem data table
public static final String FILESYSTEM_FILE_LOCAL_PATH = "local_path";
public static final String FILESYSTEM_FILE_REMOTE_PATH = "remote_path";
public static final String FILESYSTEM_FILE_MODIFIED = "modified_at";
public static final String FILESYSTEM_FILE_IS_FOLDER = "is_folder";
public static final String FILESYSTEM_FILE_FOUND_RECENTLY = "found_at";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import com.nextcloud.client.account.User
import com.nextcloud.client.appinfo.AppInfo
import com.nextcloud.client.core.AsyncRunner
import com.nextcloud.client.core.Clock
import com.nextcloud.client.database.entity.SyncedFolderEntity
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.editimage.EditImageActivity
import com.nextcloud.client.files.DeepLinkHandler
Expand All @@ -78,6 +79,7 @@ import com.nextcloud.model.WorkerState.OfflineOperationsCompleted
import com.nextcloud.ui.composeActivity.ComposeProcessTextAlias
import com.nextcloud.utils.extensions.getParcelableArgument
import com.nextcloud.utils.extensions.isActive
import com.nextcloud.utils.extensions.isDialogFragmentReady
import com.nextcloud.utils.extensions.lastFragment
import com.nextcloud.utils.extensions.logFileSize
import com.nextcloud.utils.extensions.navigateToAllFiles
Expand Down Expand Up @@ -114,6 +116,7 @@ import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask
import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask.CheckAvailableSpaceListener
import com.owncloud.android.ui.asynctasks.FetchRemoteFileTask
import com.owncloud.android.ui.asynctasks.GetRemoteFileTask
import com.owncloud.android.ui.dialog.ConfirmationDialogFragment
import com.owncloud.android.ui.dialog.DeleteBatchTracker
import com.owncloud.android.ui.dialog.SendShareDialog.SendShareDialogDownloader
import com.owncloud.android.ui.dialog.SortingOrderDialogFragment.OnSortingOrderListener
Expand Down Expand Up @@ -2193,6 +2196,63 @@ class FileDisplayActivity :
}
}

override fun onAutoUploadFolderRemoved(
entities: List<SyncedFolderEntity>,
filesToRemove: List<OCFile>,
onlyLocalCopy: Boolean
) {
val dialog = ConfirmationDialogFragment.newInstance(
messageResId = R.string.auto_upload_delete_dialog_description,
messageArguments = null,
titleResId = R.string.auto_upload_delete_dialog_title,
titleIconId = R.drawable.ic_info,
positiveButtonTextId = R.string.common_delete,
negativeButtonTextId = R.string.common_cancel,
neutralButtonTextId = -1
)

dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener {
override fun onConfirmation(callerTag: String?) {
lifecycleScope.launch(Dispatchers.IO) {
entities.forEach { entity ->
entity.id?.toLong()?.let {
fileUploadHelper.removeEntityFromUploadEntities(it)
syncedFolderProvider.deleteSyncedFolder(it)
}
}

withContext(Dispatchers.Main) {
connectivityService.isNetworkAndServerAvailable { isAvailable ->
if (isAvailable) {
fileOperationsHelper?.removeFiles(
filesToRemove,
onlyLocalCopy,
true
)
} else {
if (onlyLocalCopy) {
fileOperationsHelper?.removeFiles(filesToRemove, true, true)
} else {
filesToRemove.forEach { file ->
fileDataStorageManager.addRemoveFileOfflineOperation(file)
}
}
}
onFilesRemoved()
}
}
}
}

override fun onNeutral(callerTag: String?) = Unit
override fun onCancel(callerTag: String?) = Unit
})

if (isDialogFragmentReady(dialog)) {
dialog.show(supportFragmentManager, null)
}
}

private fun onRestoreFileVersionOperationFinish(result: RemoteOperationResult<*>) {
if (result.isSuccess) {
val file = getFile()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@

package com.owncloud.android.ui.activity

import com.nextcloud.client.database.entity.SyncedFolderEntity
import com.owncloud.android.datamodel.OCFile

interface OnFilesRemovedListener {
fun onFilesRemoved()
fun onAutoUploadFolderRemoved(
entities: List<SyncedFolderEntity>,
filesToRemove: List<OCFile>,
onlyLocalCopy: Boolean
)
}
Loading