Skip to content

Commit 6921155

Browse files
Merge pull request #15861 from nextcloud/fix/insert-custom-folder-db
fix: insert custom folder db
2 parents 978c574 + fa54e8a commit 6921155

File tree

8 files changed

+522
-198
lines changed

8 files changed

+522
-198
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.client.jobs.autoUpload
9+
10+
import com.nextcloud.utils.extensions.shouldSkipFile
11+
import com.nextcloud.utils.extensions.toLocalPath
12+
import com.owncloud.android.datamodel.FilesystemDataProvider
13+
import com.owncloud.android.datamodel.SyncedFolder
14+
import com.owncloud.android.lib.common.utils.Log_OC
15+
import java.io.IOException
16+
import java.nio.file.AccessDeniedException
17+
import java.nio.file.FileVisitOption
18+
import java.nio.file.FileVisitResult
19+
import java.nio.file.Files
20+
import java.nio.file.Path
21+
import java.nio.file.Paths
22+
import java.nio.file.SimpleFileVisitor
23+
import java.nio.file.attribute.BasicFileAttributes
24+
25+
@Suppress("TooGenericExceptionCaught", "MagicNumber", "ReturnCount")
26+
class AutoUploadHelper {
27+
companion object {
28+
private const val TAG = "AutoUploadHelper"
29+
private const val MAX_DEPTH = 100
30+
}
31+
32+
fun insertCustomFolderIntoDB(folder: SyncedFolder, filesystemDataProvider: FilesystemDataProvider?): Int {
33+
val path = Paths.get(folder.localPath)
34+
35+
if (!Files.exists(path)) {
36+
Log_OC.w(TAG, "Folder does not exist: ${folder.localPath}")
37+
return 0
38+
}
39+
40+
if (!Files.isReadable(path)) {
41+
Log_OC.w(TAG, "Folder is not readable: ${folder.localPath}")
42+
return 0
43+
}
44+
45+
val excludeHidden = folder.isExcludeHidden
46+
47+
var fileCount = 0
48+
var skipCount = 0
49+
var errorCount = 0
50+
51+
try {
52+
Files.walkFileTree(
53+
path,
54+
setOf(FileVisitOption.FOLLOW_LINKS),
55+
MAX_DEPTH,
56+
object : SimpleFileVisitor<Path>() {
57+
58+
override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes?): FileVisitResult {
59+
if (excludeHidden && dir != path && dir.toFile().isHidden) {
60+
Log_OC.d(TAG, "Skipping hidden directory: ${dir.fileName}")
61+
skipCount++
62+
return FileVisitResult.SKIP_SUBTREE
63+
}
64+
65+
return FileVisitResult.CONTINUE
66+
}
67+
68+
override fun visitFile(file: Path, attrs: BasicFileAttributes?): FileVisitResult {
69+
try {
70+
val javaFile = file.toFile()
71+
val lastModified = attrs?.lastModifiedTime()?.toMillis() ?: javaFile.lastModified()
72+
val creationTime = attrs?.creationTime()?.toMillis()
73+
74+
if (folder.shouldSkipFile(javaFile, lastModified, creationTime)) {
75+
skipCount++
76+
return FileVisitResult.CONTINUE
77+
}
78+
79+
val localPath = file.toLocalPath()
80+
81+
filesystemDataProvider?.storeOrUpdateFileValue(
82+
localPath,
83+
lastModified,
84+
javaFile.isDirectory,
85+
folder
86+
)
87+
88+
fileCount++
89+
90+
if (fileCount % 100 == 0) {
91+
Log_OC.d(TAG, "Processed $fileCount files so far...")
92+
}
93+
} catch (e: Exception) {
94+
Log_OC.e(TAG, "Error processing file: $file", e)
95+
errorCount++
96+
}
97+
98+
return FileVisitResult.CONTINUE
99+
}
100+
101+
override fun visitFileFailed(file: Path, exc: IOException?): FileVisitResult {
102+
when (exc) {
103+
is AccessDeniedException -> {
104+
Log_OC.w(TAG, "Access denied: $file")
105+
}
106+
else -> {
107+
Log_OC.e(TAG, "Failed to visit file: $file", exc)
108+
}
109+
}
110+
errorCount++
111+
return FileVisitResult.CONTINUE
112+
}
113+
114+
override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult {
115+
if (exc != null) {
116+
Log_OC.e(TAG, "Error after visiting directory: $dir", exc)
117+
errorCount++
118+
}
119+
return FileVisitResult.CONTINUE
120+
}
121+
}
122+
)
123+
124+
Log_OC.d(
125+
TAG,
126+
"Scan complete for ${folder.localPath}: " +
127+
"$fileCount files processed, $skipCount skipped, $errorCount errors"
128+
)
129+
} catch (e: Exception) {
130+
Log_OC.e(TAG, "Error walking file tree: ${folder.localPath}", e)
131+
}
132+
133+
return fileCount
134+
}
135+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ class AutoUploadWorker(
7676
private const val NOTIFICATION_ID = 266
7777
}
7878

79+
private val helper = AutoUploadHelper()
7980
private lateinit var syncedFolder: SyncedFolder
8081
private val notificationManager by lazy {
8182
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -209,7 +210,7 @@ class AutoUploadWorker(
209210
private suspend fun collectFileChangesFromContentObserverWork(contentUris: Array<String>?) = try {
210211
withContext(Dispatchers.IO) {
211212
if (contentUris.isNullOrEmpty()) {
212-
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder)
213+
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper)
213214
} else {
214215
val isContentUrisStored = FilesSyncHelper.insertChangedEntries(syncedFolder, contentUris)
215216
if (!isContentUrisStored) {
@@ -218,7 +219,7 @@ class AutoUploadWorker(
218219
"changed content uris not stored, fallback to insert all db entries to not lose files"
219220
)
220221

221-
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder)
222+
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper)
222223
}
223224
}
224225
syncedFolder.lastScanTimestampMs = System.currentTimeMillis()

app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.owncloud.android.datamodel.OCFile
1111
import com.owncloud.android.lib.common.utils.Log_OC
1212
import com.owncloud.android.utils.DisplayUtils
1313
import java.io.File
14+
import java.nio.file.Path
1415

1516
fun OCFile?.logFileSize(tag: String) {
1617
val size = DisplayUtils.bytesToHumanReadable(this?.fileLength ?: -1)
@@ -23,3 +24,5 @@ fun File?.logFileSize(tag: String) {
2324
val rawByte = this?.length() ?: -1
2425
Log_OC.d(tag, "onSaveInstanceState: $size, raw byte $rawByte")
2526
}
27+
28+
fun Path.toLocalPath(): String = toAbsolutePath().toString()

app/src/main/java/com/nextcloud/utils/extensions/SyncedFolderExtensions.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,44 @@ import com.nextcloud.client.network.ConnectivityService
1313
import com.owncloud.android.R
1414
import com.owncloud.android.datamodel.SyncedFolder
1515
import com.owncloud.android.datamodel.SyncedFolderDisplayItem
16+
import com.owncloud.android.lib.common.utils.Log_OC
1617
import java.io.File
1718

19+
private const val TAG = "SyncedFolderExtensions"
20+
21+
/**
22+
* Determines whether a file should be skipped during auto-upload based on folder settings.
23+
*/
24+
@Suppress("ReturnCount")
25+
fun SyncedFolder.shouldSkipFile(file: File, lastModified: Long, creationTime: Long?): Boolean {
26+
if (isExcludeHidden && file.isHidden) {
27+
Log_OC.d(TAG, "Skipping hidden: ${file.absolutePath}")
28+
return true
29+
}
30+
31+
// If "upload existing files" is DISABLED, only upload files created after enabled time
32+
if (!isExisting) {
33+
if (creationTime != null) {
34+
if (creationTime < enabledTimestampMs) {
35+
Log_OC.d(TAG, "Skipping pre-existing file (creation < enabled): ${file.absolutePath}")
36+
return true
37+
}
38+
} else {
39+
Log_OC.w(TAG, "file sent for upload - cannot determine creation time: ${file.absolutePath}")
40+
return false
41+
}
42+
}
43+
44+
// Skip files that haven't changed since last scan (already processed)
45+
// BUT only if this is not the first scan
46+
if (lastScanTimestampMs != -1L && lastModified < lastScanTimestampMs) {
47+
Log_OC.d(TAG, "Skipping unchanged file (last modified < last scan): ${file.absolutePath}")
48+
return true
49+
}
50+
51+
return false
52+
}
53+
1854
fun List<SyncedFolderDisplayItem>.filterEnabledOrWithoutEnabledParent(): List<SyncedFolderDisplayItem> = filter {
1955
it.isEnabled || !hasEnabledParent(it.localPath)
2056
}

app/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,28 @@ public boolean isChargingOnly() {
179179
return this.chargingOnly;
180180
}
181181

182+
/**
183+
* Indicates whether the "Also upload existing files" option is enabled for this folder.
184+
*
185+
* <p>
186+
* This flag controls how files in the folder are treated when auto-upload is enabled:
187+
* <ul>
188+
* <li>If {@code true} (existing files are included):
189+
* <ul>
190+
* <li>All files in the folder, regardless of creation date, will be uploaded.</li>
191+
* </ul>
192+
* </li>
193+
* <li>If {@code false} (existing files are skipped):
194+
* <ul>
195+
* <li>Only files created or added after the folder was enabled will be uploaded.</li>
196+
* <li>Files that existed before enabling will be skipped, based on their creation time.</li>
197+
* </ul>
198+
* </li>
199+
* </ul>
200+
* </p>
201+
*
202+
* @return {@code true} if existing files should also be uploaded, {@code false} otherwise
203+
*/
182204
public boolean isExisting() {
183205
return this.existing;
184206
}

0 commit comments

Comments
 (0)