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
@@ -0,0 +1,135 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.client.jobs.autoUpload

import com.nextcloud.utils.extensions.shouldSkipFile
import com.nextcloud.utils.extensions.toLocalPath
import com.owncloud.android.datamodel.FilesystemDataProvider
import com.owncloud.android.datamodel.SyncedFolder
import com.owncloud.android.lib.common.utils.Log_OC
import java.io.IOException
import java.nio.file.AccessDeniedException
import java.nio.file.FileVisitOption
import java.nio.file.FileVisitResult
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.SimpleFileVisitor
import java.nio.file.attribute.BasicFileAttributes

@Suppress("TooGenericExceptionCaught", "MagicNumber", "ReturnCount")
class AutoUploadHelper {
companion object {
private const val TAG = "AutoUploadHelper"
private const val MAX_DEPTH = 100
}

fun insertCustomFolderIntoDB(folder: SyncedFolder, filesystemDataProvider: FilesystemDataProvider?): Int {
val path = Paths.get(folder.localPath)

if (!Files.exists(path)) {
Log_OC.w(TAG, "Folder does not exist: ${folder.localPath}")
return 0
}

if (!Files.isReadable(path)) {
Log_OC.w(TAG, "Folder is not readable: ${folder.localPath}")
return 0
}

val excludeHidden = folder.isExcludeHidden

var fileCount = 0
var skipCount = 0
var errorCount = 0

try {
Files.walkFileTree(
path,
setOf(FileVisitOption.FOLLOW_LINKS),
MAX_DEPTH,
object : SimpleFileVisitor<Path>() {

override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes?): FileVisitResult {
if (excludeHidden && dir != path && dir.toFile().isHidden) {
Log_OC.d(TAG, "Skipping hidden directory: ${dir.fileName}")
skipCount++
return FileVisitResult.SKIP_SUBTREE
}

return FileVisitResult.CONTINUE
}

override fun visitFile(file: Path, attrs: BasicFileAttributes?): FileVisitResult {
try {
val javaFile = file.toFile()
val lastModified = attrs?.lastModifiedTime()?.toMillis() ?: javaFile.lastModified()
val creationTime = attrs?.creationTime()?.toMillis()

if (folder.shouldSkipFile(javaFile, lastModified, creationTime)) {
skipCount++
return FileVisitResult.CONTINUE
}

val localPath = file.toLocalPath()

filesystemDataProvider?.storeOrUpdateFileValue(
localPath,
lastModified,
javaFile.isDirectory,
folder
)

fileCount++

if (fileCount % 100 == 0) {
Log_OC.d(TAG, "Processed $fileCount files so far...")
}
} catch (e: Exception) {
Log_OC.e(TAG, "Error processing file: $file", e)
errorCount++
}

return FileVisitResult.CONTINUE
}

override fun visitFileFailed(file: Path, exc: IOException?): FileVisitResult {
when (exc) {
is AccessDeniedException -> {
Log_OC.w(TAG, "Access denied: $file")
}
else -> {
Log_OC.e(TAG, "Failed to visit file: $file", exc)
}
}
errorCount++
return FileVisitResult.CONTINUE
}

override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult {
if (exc != null) {
Log_OC.e(TAG, "Error after visiting directory: $dir", exc)
errorCount++
}
return FileVisitResult.CONTINUE
}
}
)

Log_OC.d(
TAG,
"Scan complete for ${folder.localPath}: " +
"$fileCount files processed, $skipCount skipped, $errorCount errors"
)
} catch (e: Exception) {
Log_OC.e(TAG, "Error walking file tree: ${folder.localPath}", e)
}

return fileCount
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class AutoUploadWorker(
private const val NOTIFICATION_ID = 266
}

private val helper = AutoUploadHelper()
private lateinit var syncedFolder: SyncedFolder
private val notificationManager by lazy {
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
Expand Down Expand Up @@ -209,7 +210,7 @@ class AutoUploadWorker(
private suspend fun collectFileChangesFromContentObserverWork(contentUris: Array<String>?) = try {
withContext(Dispatchers.IO) {
if (contentUris.isNullOrEmpty()) {
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder)
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper)
} else {
val isContentUrisStored = FilesSyncHelper.insertChangedEntries(syncedFolder, contentUris)
if (!isContentUrisStored) {
Expand All @@ -218,7 +219,7 @@ class AutoUploadWorker(
"changed content uris not stored, fallback to insert all db entries to not lose files"
)

FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder)
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper)
}
}
syncedFolder.lastScanTimestampMs = System.currentTimeMillis()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.utils.DisplayUtils
import java.io.File
import java.nio.file.Path

fun OCFile?.logFileSize(tag: String) {
val size = DisplayUtils.bytesToHumanReadable(this?.fileLength ?: -1)
Expand All @@ -23,3 +24,5 @@ fun File?.logFileSize(tag: String) {
val rawByte = this?.length() ?: -1
Log_OC.d(tag, "onSaveInstanceState: $size, raw byte $rawByte")
}

fun Path.toLocalPath(): String = toAbsolutePath().toString()
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,44 @@ import com.nextcloud.client.network.ConnectivityService
import com.owncloud.android.R
import com.owncloud.android.datamodel.SyncedFolder
import com.owncloud.android.datamodel.SyncedFolderDisplayItem
import com.owncloud.android.lib.common.utils.Log_OC
import java.io.File

private const val TAG = "SyncedFolderExtensions"

/**
* Determines whether a file should be skipped during auto-upload based on folder settings.
*/
@Suppress("ReturnCount")
fun SyncedFolder.shouldSkipFile(file: File, lastModified: Long, creationTime: Long?): Boolean {
if (isExcludeHidden && file.isHidden) {
Log_OC.d(TAG, "Skipping hidden: ${file.absolutePath}")
return true
}

// If "upload existing files" is DISABLED, only upload files created after enabled time
if (!isExisting) {
if (creationTime != null) {
if (creationTime < enabledTimestampMs) {
Log_OC.d(TAG, "Skipping pre-existing file (creation < enabled): ${file.absolutePath}")
return true
}
} else {
Log_OC.w(TAG, "file sent for upload - cannot determine creation time: ${file.absolutePath}")
return false
}
}

// Skip files that haven't changed since last scan (already processed)
// BUT only if this is not the first scan
if (lastScanTimestampMs != -1L && lastModified < lastScanTimestampMs) {
Log_OC.d(TAG, "Skipping unchanged file (last modified < last scan): ${file.absolutePath}")
return true
}

return false
}

fun List<SyncedFolderDisplayItem>.filterEnabledOrWithoutEnabledParent(): List<SyncedFolderDisplayItem> = filter {
it.isEnabled || !hasEnabledParent(it.localPath)
}
Expand Down
22 changes: 22 additions & 0 deletions app/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,28 @@ public boolean isChargingOnly() {
return this.chargingOnly;
}

/**
* Indicates whether the "Also upload existing files" option is enabled for this folder.
*
* <p>
* This flag controls how files in the folder are treated when auto-upload is enabled:
* <ul>
* <li>If {@code true} (existing files are included):
* <ul>
* <li>All files in the folder, regardless of creation date, will be uploaded.</li>
* </ul>
* </li>
* <li>If {@code false} (existing files are skipped):
* <ul>
* <li>Only files created or added after the folder was enabled will be uploaded.</li>
* <li>Files that existed before enabling will be skipped, based on their creation time.</li>
* </ul>
* </li>
* </ul>
* </p>
*
* @return {@code true} if existing files should also be uploaded, {@code false} otherwise
*/
public boolean isExisting() {
return this.existing;
}
Expand Down
Loading
Loading