Skip to content

Commit e495762

Browse files
Merge pull request #16150 from nextcloud/backport/16121/stable-3.35
[stable-3.35] fix: file system provider
2 parents abc8c65 + 1b28c07 commit e495762

11 files changed

Lines changed: 287 additions & 550 deletions

File tree

app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@
88
package com.nextcloud.client.database.dao
99

1010
import androidx.room.Dao
11+
import androidx.room.Insert
12+
import androidx.room.OnConflictStrategy
1113
import androidx.room.Query
1214
import com.nextcloud.client.database.entity.FilesystemEntity
1315
import com.owncloud.android.db.ProviderMeta
1416

1517
@Dao
1618
interface FileSystemDao {
19+
@Insert(onConflict = OnConflictStrategy.REPLACE)
20+
fun insertOrReplace(filesystemEntity: FilesystemEntity)
21+
1722
@Query(
1823
"""
1924
SELECT *
@@ -37,4 +42,15 @@ interface FileSystemDao {
3742
"""
3843
)
3944
suspend fun markFileAsUploaded(localPath: String, syncedFolderId: String)
45+
46+
@Query(
47+
"""
48+
SELECT *
49+
FROM ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME}
50+
WHERE ${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH} = :localPath
51+
AND ${ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID} = :syncedFolderId
52+
LIMIT 1
53+
"""
54+
)
55+
fun getFileByPathAndFolder(localPath: String, syncedFolderId: String): FilesystemEntity?
4056
}

app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ class BackgroundJobFactory @Inject constructor(
179179
powerManagementService = powerManagementService,
180180
syncedFolderProvider = syncedFolderProvider,
181181
backgroundJobManager = backgroundJobManager.get(),
182-
repository = FileSystemRepository(dao = database.fileSystemDao()),
182+
repository = FileSystemRepository(dao = database.fileSystemDao(), context),
183183
viewThemeUtils = viewThemeUtils.get()
184184
)
185185

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

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77

88
package com.nextcloud.client.jobs.autoUpload
99

10-
import com.nextcloud.utils.extensions.shouldSkipFile
10+
import android.provider.MediaStore
11+
import androidx.core.net.toUri
1112
import com.nextcloud.utils.extensions.toLocalPath
12-
import com.owncloud.android.datamodel.FilesystemDataProvider
13+
import com.owncloud.android.datamodel.MediaFolderType
1314
import com.owncloud.android.datamodel.SyncedFolder
1415
import com.owncloud.android.lib.common.utils.Log_OC
1516
import java.io.IOException
@@ -29,7 +30,66 @@ class AutoUploadHelper {
2930
private const val MAX_DEPTH = 100
3031
}
3132

32-
fun insertCustomFolderIntoDB(folder: SyncedFolder, filesystemDataProvider: FilesystemDataProvider?): Int {
33+
fun insertEntries(folder: SyncedFolder, repository: FileSystemRepository) {
34+
val enabledTimestampMs = folder.enabledTimestampMs
35+
if (!folder.isEnabled || (!folder.isExisting && enabledTimestampMs < 0)) {
36+
Log_OC.w(
37+
TAG,
38+
"Skipping insertDBEntries: enabled=${folder.isEnabled}, " +
39+
"exists=${folder.isExisting}, enabledTs=$enabledTimestampMs"
40+
)
41+
return
42+
}
43+
44+
when (folder.type) {
45+
MediaFolderType.IMAGE -> {
46+
repository.insertFromUri(MediaStore.Images.Media.INTERNAL_CONTENT_URI, folder)
47+
repository.insertFromUri(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, folder)
48+
}
49+
50+
MediaFolderType.VIDEO -> {
51+
repository.insertFromUri(MediaStore.Video.Media.INTERNAL_CONTENT_URI, folder)
52+
repository.insertFromUri(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, folder)
53+
}
54+
55+
else -> {
56+
insertCustomFolderIntoDB(folder, repository)
57+
}
58+
}
59+
}
60+
61+
/**
62+
* Attempts to get the file path from a content URI string (e.g., content://media/external/images/media/2281)
63+
* and checks its type. If the conditions are met, the file is stored for auto-upload.
64+
* <p>
65+
* If any attempt fails, the method returns {@code false}.
66+
*
67+
* @param syncedFolder The folder marked for auto-upload.
68+
* @param contentUris An array of content URI strings collected from
69+
* {@link ContentObserverWork##checkAndTriggerAutoUpload()}.
70+
* @return {@code true} if all changed content URIs were successfully stored; {@code false} otherwise.
71+
*/
72+
fun insertChangedEntries(
73+
syncedFolder: SyncedFolder,
74+
contentUris: Array<String>?,
75+
repository: FileSystemRepository
76+
): Boolean {
77+
contentUris?.forEach { uriString ->
78+
try {
79+
val uri = uriString.toUri()
80+
repository.insertFromUri(uri, syncedFolder, true)
81+
} catch (e: Exception) {
82+
Log_OC.e(TAG, "Invalid URI: $uriString", e)
83+
return false
84+
}
85+
}
86+
87+
Log_OC.d(TAG, "Changed content URIs successfully stored")
88+
89+
return true
90+
}
91+
92+
fun insertCustomFolderIntoDB(folder: SyncedFolder, repository: FileSystemRepository): Int {
3393
val path = Paths.get(folder.localPath)
3494

3595
if (!Files.exists(path)) {
@@ -70,20 +130,9 @@ class AutoUploadHelper {
70130
val javaFile = file.toFile()
71131
val lastModified = attrs?.lastModifiedTime()?.toMillis() ?: javaFile.lastModified()
72132
val creationTime = attrs?.creationTime()?.toMillis()
73-
74-
if (folder.shouldSkipFile(javaFile, lastModified, creationTime)) {
75-
skipCount++
76-
return FileVisitResult.CONTINUE
77-
}
78-
79133
val localPath = file.toLocalPath()
80134

81-
filesystemDataProvider?.storeOrUpdateFileValue(
82-
localPath,
83-
lastModified,
84-
javaFile.isDirectory,
85-
folder
86-
)
135+
repository.insertOrReplace(localPath, lastModified, creationTime, folder)
87136

88137
fileCount++
89138

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ import com.owncloud.android.lib.common.utils.Log_OC
4242
import com.owncloud.android.operations.UploadFileOperation
4343
import com.owncloud.android.ui.activity.SettingsActivity
4444
import com.owncloud.android.utils.FileStorageUtils
45-
import com.owncloud.android.utils.FilesSyncHelper
4645
import com.owncloud.android.utils.MimeType
4746
import com.owncloud.android.utils.theme.ViewThemeUtils
4847
import kotlinx.coroutines.Dispatchers
@@ -230,16 +229,16 @@ class AutoUploadWorker(
230229
private suspend fun collectFileChangesFromContentObserverWork(contentUris: Array<String>?) = try {
231230
withContext(Dispatchers.IO) {
232231
if (contentUris.isNullOrEmpty()) {
233-
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper)
232+
helper.insertEntries(syncedFolder, repository)
234233
} else {
235-
val isContentUrisStored = FilesSyncHelper.insertChangedEntries(syncedFolder, contentUris)
234+
val isContentUrisStored = helper.insertChangedEntries(syncedFolder, contentUris, repository)
236235
if (!isContentUrisStored) {
237236
Log_OC.w(
238237
TAG,
239238
"changed content uris not stored, fallback to insert all db entries to not lose files"
240239
)
241240

242-
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper)
241+
helper.insertEntries(syncedFolder, repository)
243242
}
244243
}
245244
syncedFolder.lastScanTimestampMs = System.currentTimeMillis()

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

Lines changed: 145 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,27 @@
77

88
package com.nextcloud.client.jobs.autoUpload
99

10+
import android.content.Context
11+
import android.net.Uri
12+
import android.provider.MediaStore
1013
import com.nextcloud.client.database.dao.FileSystemDao
14+
import com.nextcloud.client.database.entity.FilesystemEntity
15+
import com.nextcloud.utils.extensions.shouldSkipFile
16+
import com.nextcloud.utils.extensions.toFile
1117
import com.owncloud.android.datamodel.SyncedFolder
1218
import com.owncloud.android.lib.common.utils.Log_OC
1319
import com.owncloud.android.utils.SyncedFolderUtils
1420
import java.io.File
21+
import java.util.zip.CRC32
1522

16-
class FileSystemRepository(private val dao: FileSystemDao) {
23+
@Suppress("TooGenericExceptionCaught", "NestedBlockDepth", "MagicNumber", "ReturnCount")
24+
class FileSystemRepository(private val dao: FileSystemDao, private val context: Context) {
1725

1826
companion object {
1927
private const val TAG = "FilesystemRepository"
2028
const val BATCH_SIZE = 50
2129
}
2230

23-
@Suppress("NestedBlockDepth")
2431
suspend fun getFilePathsWithIds(syncedFolder: SyncedFolder, lastId: Int): List<Pair<String, Int>> {
2532
val syncedFolderId = syncedFolder.id.toString()
2633
Log_OC.d(TAG, "Fetching candidate files for syncedFolderId = $syncedFolderId")
@@ -52,7 +59,6 @@ class FileSystemRepository(private val dao: FileSystemDao) {
5259
return filtered
5360
}
5461

55-
@Suppress("TooGenericExceptionCaught")
5662
suspend fun markFileAsUploaded(localPath: String, syncedFolder: SyncedFolder) {
5763
val syncedFolderIdStr = syncedFolder.id.toString()
5864

@@ -63,4 +69,140 @@ class FileSystemRepository(private val dao: FileSystemDao) {
6369
Log_OC.e(TAG, "Error marking file as uploaded: ${e.message}", e)
6470
}
6571
}
72+
73+
@JvmOverloads
74+
fun insertFromUri(uri: Uri, syncedFolder: SyncedFolder, checkFileType: Boolean = false) {
75+
val projection = arrayOf(
76+
MediaStore.MediaColumns.DATA,
77+
MediaStore.MediaColumns.DATE_MODIFIED,
78+
MediaStore.MediaColumns.DATE_ADDED
79+
)
80+
81+
var syncedPath = syncedFolder.localPath
82+
if (syncedPath.isNullOrEmpty()) {
83+
Log_OC.w(TAG, "Synced folder path is null or empty")
84+
return
85+
}
86+
87+
if (!syncedPath.endsWith(File.separator)) {
88+
syncedPath += File.separator
89+
}
90+
91+
val selection = "${MediaStore.MediaColumns.DATA} LIKE ?"
92+
val selectionArgs = arrayOf("$syncedPath%")
93+
94+
Log_OC.d(TAG, "Querying MediaStore for files in: $syncedPath, uri: $uri")
95+
96+
val cursor = context.contentResolver.query(
97+
uri,
98+
projection,
99+
selection,
100+
selectionArgs,
101+
null
102+
)
103+
104+
cursor?.use {
105+
val idxData = cursor.getColumnIndex(MediaStore.MediaColumns.DATA)
106+
val idxModified = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED)
107+
val idxAdded = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED)
108+
109+
if (idxData == -1) {
110+
Log_OC.e(TAG, "MediaStore column DATA missing — cannot process URI: $uri")
111+
return
112+
}
113+
114+
while (cursor.moveToNext()) {
115+
val filePath = cursor.getString(idxData)
116+
117+
val lastModifiedMs = if (idxModified != -1) {
118+
cursor.getLong(idxModified) * 1000
119+
} else {
120+
null
121+
}
122+
123+
val creationTimeMs = if (idxAdded != -1) {
124+
cursor.getLong(idxAdded) * 1000
125+
} else {
126+
null
127+
}
128+
129+
Log_OC.d(
130+
TAG,
131+
"Found file: $filePath (created=$creationTimeMs, modified=$lastModifiedMs)"
132+
)
133+
134+
insertOrReplace(filePath, lastModifiedMs, creationTimeMs, syncedFolder, checkFileType)
135+
}
136+
}
137+
}
138+
139+
fun insertOrReplace(
140+
localPath: String?,
141+
lastModified: Long?,
142+
creationTime: Long?,
143+
syncedFolder: SyncedFolder,
144+
checkFileType: Boolean = false
145+
) {
146+
try {
147+
val file = localPath?.toFile()
148+
if (file == null) {
149+
Log_OC.w(TAG, "file null, cannot insert or replace: $localPath")
150+
return
151+
}
152+
153+
if (checkFileType && !syncedFolder.containsTypedFile(file, localPath)) {
154+
Log_OC.w(TAG, "synced folder not contains typed file: $localPath")
155+
return
156+
}
157+
158+
val fileModified = (lastModified ?: file.lastModified())
159+
val shouldSkipFileBasedOnFolderSettings = syncedFolder.shouldSkipFile(file, fileModified, creationTime)
160+
if (shouldSkipFileBasedOnFolderSettings) {
161+
return
162+
}
163+
164+
val entity = dao.getFileByPathAndFolder(localPath, syncedFolder.id.toString())
165+
if (entity != null && entity.fileSentForUpload == 1) {
166+
Log_OC.w(
167+
TAG,
168+
"file already uploaded path: $localPath, " +
169+
"syncedFolder: ${syncedFolder.localPath}, ${syncedFolder.id}"
170+
)
171+
return
172+
}
173+
174+
val crc = getFileChecksum(file)
175+
176+
val newEntity = FilesystemEntity(
177+
id = entity?.id,
178+
localPath = localPath,
179+
fileIsFolder = if (file.isDirectory) 1 else 0,
180+
fileFoundRecently = System.currentTimeMillis(),
181+
fileSentForUpload = 0,
182+
syncedFolderId = syncedFolder.id.toString(),
183+
crc32 = crc?.toString(),
184+
fileModified = fileModified
185+
)
186+
187+
Log_OC.d(TAG, "inserting new file system entity: $newEntity")
188+
189+
dao.insertOrReplace(newEntity)
190+
} catch (e: Exception) {
191+
Log_OC.e(TAG, "Failed to insert/update file: $localPath", e)
192+
}
193+
}
194+
195+
private fun getFileChecksum(file: File): Long? = try {
196+
file.inputStream().use { fis ->
197+
val crc = CRC32()
198+
val buffer = ByteArray(64 * 1024)
199+
var bytesRead: Int
200+
while (fis.read(buffer).also { bytesRead = it } > 0) {
201+
crc.update(buffer, 0, bytesRead)
202+
}
203+
crc.value
204+
}
205+
} catch (_: Exception) {
206+
null
207+
}
66208
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ private const val TAG = "SyncedFolderExtensions"
2323
*/
2424
@Suppress("ReturnCount")
2525
fun SyncedFolder.shouldSkipFile(file: File, lastModified: Long, creationTime: Long?): Boolean {
26+
Log_OC.d(TAG, "Checking file: ${file.name}, lastModified=$lastModified, lastScan=$lastScanTimestampMs")
27+
2628
if (isExcludeHidden && file.isHidden) {
2729
Log_OC.d(TAG, "Skipping hidden: ${file.absolutePath}")
2830
return true

0 commit comments

Comments
 (0)