Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7c0bcef
feature/1475-delete-uploaded-files: Menu item and popup
daniele-verducci Aug 18, 2026
3e12905
feature/1475-delete-uploaded-files: WIP proof of concept
daniele-verducci Aug 19, 2026
a88f115
feature/1475-delete-uploaded-files: working deletion based only on fi…
daniele-verducci Aug 20, 2026
7143e08
feature/1475-delete-uploaded-files: multi folder deletion, refactoring
daniele-verducci Aug 20, 2026
e40a3a4
feature/1475-delete-uploaded-files: WIP (working folder refresh)
daniele-verducci Aug 20, 2026
25ed0da
feature/1475-delete-uploaded-files: Working files checks
daniele-verducci Aug 20, 2026
ee85107
feature/1475-delete-uploaded-files: Cleanup
daniele-verducci Aug 20, 2026
f8a842a
feature/1475-delete-uploaded-files: refresh subfolders as needed
daniele-verducci Aug 21, 2026
1d34e1c
feature/1475-delete-uploaded-files: Added option to system manage spa…
daniele-verducci Aug 21, 2026
3d1bfa9
feature/1475-delete-uploaded-files: Moved logic to background worker,…
daniele-verducci Aug 21, 2026
88b21e9
feature/1475-delete-uploaded-files: Passing needed objects to backgro…
daniele-verducci Aug 21, 2026
30f5371
feature/1475-delete-uploaded-files: notifications
daniele-verducci Aug 24, 2026
a60a922
feature/1475-delete-uploaded-files: lint
daniele-verducci Aug 24, 2026
9a6f249
feature/1475-delete-uploaded-files: Manage non-writeable folders
daniele-verducci Aug 24, 2026
fd2ff29
feature/1475-delete-uploaded-files: Fix user race condition
daniele-verducci Aug 25, 2026
30e4236
feature/1475-delete-uploaded-files: Stats
daniele-verducci Aug 26, 2026
ef18157
feature/1475-delete-uploaded-files: Fixed untouched files report
daniele-verducci Aug 26, 2026
2061cac
feature/1475-delete-uploaded-files: Better user notifications
daniele-verducci Aug 26, 2026
ba15e83
feature/1475-delete-uploaded-files: spotless run
daniele-verducci Aug 27, 2026
91cf318
feature/1475-delete-uploaded-files: show runtime in stats
daniele-verducci Aug 27, 2026
e13be39
feature/1475-delete-uploaded-files: more logs & user info, fix
daniele-verducci Aug 27, 2026
0beac4f
feature/1475-delete-uploaded-files: apply to current user or all user…
daniele-verducci Aug 27, 2026
4ab3bca
feature/1475-delete-uploaded-files: fixed bug preventing removal of p…
daniele-verducci Aug 27, 2026
93e1fe5
feature/1475-delete-uploaded-files: enabled real file deletion
daniele-verducci Aug 27, 2026
dc5dd34
feature/1475-delete-uploaded-files: changed some strings, simpler com…
daniele-verducci Aug 28, 2026
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 @@ -23,6 +23,7 @@ import com.nextcloud.client.documentscan.GeneratePDFUseCase
import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork
import com.nextcloud.client.integrations.deck.DeckApi
import com.nextcloud.client.jobs.autoUpload.AutoUploadHelper
import com.nextcloud.client.jobs.autoUpload.AutoUploadLocalDeletionWorker
import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker
import com.nextcloud.client.jobs.autoUpload.FileSystemRepository
import com.nextcloud.client.jobs.download.FileDownloadWorker
Expand Down Expand Up @@ -107,6 +108,7 @@ class BackgroundJobFactory @Inject constructor(
InternalTwoWaySyncWork::class -> createInternalTwoWaySyncWork(context, workerParameters)
MetadataWorker::class -> createMetadataWorker(context, workerParameters)
FolderDownloadWorker::class -> createFolderDownloadWorker(context, workerParameters)
AutoUploadLocalDeletionWorker::class -> createAutoUploadLocalDeletionWorker(context, workerParameters)
else -> null // caller falls back to default factory
}
}
Expand Down Expand Up @@ -311,4 +313,15 @@ class BackgroundJobFactory @Inject constructor(
localBroadcastManager.get(),
params
)

private fun createAutoUploadLocalDeletionWorker(
context: Context,
params: WorkerParameters
): AutoUploadLocalDeletionWorker = AutoUploadLocalDeletionWorker(
context = context,
params = params,
userAccountManager = accountManager,
syncedFolderProvider = syncedFolderProvider,
viewThemeUtils = viewThemeUtils.get()
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,5 @@ interface BackgroundJobManager {
fun startMetadataSyncJob(currentDirPath: String)
fun downloadFolder(folder: OCFile, accountName: String)
fun cancelFolderDownload()
fun locallyDeleteAutoUploadedFiles(syncedFolders: List<SyncedFolder>)
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.nextcloud.client.account.User
import com.nextcloud.client.core.Clock
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork
import com.nextcloud.client.jobs.autoUpload.AutoUploadLocalDeletionWorker
import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker
import com.nextcloud.client.jobs.download.FileDownloadWorker
import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker
Expand Down Expand Up @@ -103,6 +104,7 @@ internal class BackgroundJobManagerImpl(
const val JOB_DOWNLOAD_FOLDER = "download_folder"
const val JOB_METADATA_SYNC = "metadata_sync"
const val JOB_INTERNAL_TWO_WAY_SYNC = "internal_two_way_sync"
const val JOB_AUTO_UPLOAD_LOCAL_DELETION = "auto_upload_local_deletion"

const val JOB_TEST = "test_job"

Expand Down Expand Up @@ -848,4 +850,33 @@ internal class BackgroundJobManagerImpl(
override fun cancelFolderDownload() {
workManager.cancelAllWorkByTag(JOB_DOWNLOAD_FOLDER)
}

override fun locallyDeleteAutoUploadedFiles(syncedFolders: List<SyncedFolder>) {
val syncedFolderIDs = syncedFolders
.filter { it.isEnabled }
.map { it.id }

val arguments = Data.Builder()
.putLongArray(AutoUploadLocalDeletionWorker.SYNCED_FOLDER_IDS, syncedFolderIDs.toLongArray())
.build()

val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

val jobName = JOB_AUTO_UPLOAD_LOCAL_DELETION + "_" + syncedFolderIDs.joinToString("-")
val request = oneTimeRequestBuilder(
jobClass = AutoUploadLocalDeletionWorker::class,
jobName = jobName
)
.setInputData(arguments)
.setConstraints(constraints)
.build()

workManager.enqueueUniqueWork(
jobName,
ExistingWorkPolicy.KEEP,
request
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Daniele Verducci <daniele.verducci@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.client.jobs.autoUpload

import android.app.Notification
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.jobs.notification.WorkerNotificationManager
import com.owncloud.android.R
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.SyncedFolderProvider
import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.operations.upload.DeleteUploadedFileOperation
import com.owncloud.android.ui.notifications.NotificationUtils
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.FileUtil
import com.owncloud.android.utils.theme.ViewThemeUtils
import java.io.File

class AutoUploadLocalDeletionWorker(
private val context: Context,
params: WorkerParameters,
private val userAccountManager: UserAccountManager,
private val syncedFolderProvider: SyncedFolderProvider,
val viewThemeUtils: ViewThemeUtils
) : CoroutineWorker(context, params) {

companion object {
const val SYNCED_FOLDER_IDS = "synced_folder_IDs"
const val NOTIFICATION_ID = 267

private const val TAG = "AutoUploadLocalDeletionWorker"
}

private val notificationManager = WorkerNotificationManager(
NOTIFICATION_ID,
context,
viewThemeUtils,
R.string.autoupload_delete_uploaded_notif_ticker,
NotificationUtils.NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS
)

override suspend fun doWork(): Result {
showNotification(
createNotification(context.getString(R.string.autoupload_delete_uploaded_notif_started_title))
)
Log_OC.d(TAG, "Started")

val syncedFolderIDs = inputData.getLongArray(SYNCED_FOLDER_IDS)
?: throw IllegalArgumentException("$SYNCED_FOLDER_IDS param is mandatory")
val syncedFolders = syncedFolderIDs
.map { syncedFolderProvider.getSyncedFolderByID(it) }

var users = HashSet<String>()
var filesPreserved = 0L
var foldersAnalyzed = 0L
var filesRemoved = 0L
var spaceFreed = 0L
val timeStarted = System.currentTimeMillis()
syncedFolders
.filterNotNull()
.filter { it.isEnabled }
.filter { FileUtil.isFolderWritable(File(it.localPath)) }
.forEach {
val sharedFolderOwner = userAccountManager.getUser(it.account).get()
users.add(sharedFolderOwner.accountName)
val fileDataStorageManager = FileDataStorageManager(sharedFolderOwner, context.contentResolver)
val op = DeleteUploadedFileOperation(
it,
context,
fileDataStorageManager
)
val res = op.run()
if (res.code != RemoteOperationResult.ResultCode.OK) {
Log_OC.d(TAG, "Failed")
showNotification(
createNotification(context.getString(R.string.autoupload_delete_uploaded_notif_error_title))
)
return Result.failure()
}
foldersAnalyzed++
filesPreserved += res.resultData.filesPreserved
filesRemoved += res.resultData.filesRemoved
spaceFreed += res.resultData.spaceFreed
}

val runTimeMs = System.currentTimeMillis() - timeStarted
showNotification(
createSuccessNotification(
users.size,
foldersAnalyzed,
filesRemoved,
filesPreserved,
spaceFreed,
runTimeMs
)
)
Log_OC.d(
TAG,
"Success: users=$users, foldersAnalyzed=$foldersAnalyzed, filesPreserved=$filesPreserved, " +
"filesRemoved=$filesRemoved, spaceFreed=$spaceFreed bytes, " +
"runTime=${DisplayUtils.unixTimeDurationToHumanReadable(context, runTimeMs)}"
)
return Result.success()
}

private fun createSuccessNotification(
users: Int,
foldersRemoved: Long,
filesRemoved: Long,
filesPreserved: Long,
spaceFreed: Long,
timeElapsed: Long
): Notification {
val notificationContent = context.getString(
R.string.autoupload_delete_uploaded_notif_ended_content,
DisplayUtils.bytesToHumanReadable(spaceFreed),
filesRemoved,
foldersRemoved,
users
)
return createNotification(
title = context.getString(R.string.autoupload_delete_uploaded_notif_ended_title),
content = notificationContent
)
}

private fun createNotification(title: String, content: String? = null): Notification =
notificationManager.notificationBuilder
.setContentTitle(title)
.setContentText(content)
.setSmallIcon(R.drawable.ic_delete)
.setSound(null)
.setVibrate(null)
.setOnlyAlertOnce(true)
.setSilent(true)
.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS)
.build()

private fun showNotification(notification: Notification) = notificationManager.showNotification()
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ class SyncFolderHelper(private val context: Context) {
private const val TAG = "SyncFolderHelper"
}

/**
* Computes the auto upload remote path for a given file based on the current syncedFolder folder settings.
* Note that if the user changed the syncedFolder's settings after the file was already uploaded,
* this may not reflect the actual uploaded file's path.
* @param syncedFolder containing the file
* @param file contained in the syncedFolder
* @return the remote path based on the current syncedFolder folder settings
*/
fun getAutoUploadRemotePath(syncedFolder: SyncedFolder, file: File): String {
val resources = context.resources
val isLightVersion = resources.getBoolean(R.bool.syncedFolder_light)
Expand Down
Loading
Loading