Skip to content
Merged
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
Expand Up @@ -8,12 +8,17 @@
package com.nextcloud.client.database.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.nextcloud.client.database.entity.FilesystemEntity
import com.owncloud.android.db.ProviderMeta

@Dao
interface FileSystemDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrReplace(filesystemEntity: FilesystemEntity)

@Query(
"""
SELECT *
Expand All @@ -37,4 +42,15 @@ interface FileSystemDao {
"""
)
suspend fun markFileAsUploaded(localPath: String, syncedFolderId: String)

@Query(
"""
SELECT *
FROM ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME}
WHERE ${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH} = :localPath
AND ${ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID} = :syncedFolderId
LIMIT 1
"""
)
fun getFileByPathAndFolder(localPath: String, syncedFolderId: String): FilesystemEntity?
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ class BackgroundJobFactory @Inject constructor(
powerManagementService = powerManagementService,
syncedFolderProvider = syncedFolderProvider,
backgroundJobManager = backgroundJobManager.get(),
repository = FileSystemRepository(dao = database.fileSystemDao()),
repository = FileSystemRepository(dao = database.fileSystemDao(), context),
viewThemeUtils = viewThemeUtils.get()
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@

package com.nextcloud.client.jobs.autoUpload

import com.nextcloud.utils.extensions.shouldSkipFile
import android.provider.MediaStore
import androidx.core.net.toUri
import com.nextcloud.utils.extensions.toLocalPath
import com.owncloud.android.datamodel.FilesystemDataProvider
import com.owncloud.android.datamodel.MediaFolderType
import com.owncloud.android.datamodel.SyncedFolder
import com.owncloud.android.lib.common.utils.Log_OC
import java.io.IOException
Expand All @@ -29,7 +30,66 @@ class AutoUploadHelper {
private const val MAX_DEPTH = 100
}

fun insertCustomFolderIntoDB(folder: SyncedFolder, filesystemDataProvider: FilesystemDataProvider?): Int {
fun insertEntries(folder: SyncedFolder, repository: FileSystemRepository) {
val enabledTimestampMs = folder.enabledTimestampMs
if (!folder.isEnabled || (!folder.isExisting && enabledTimestampMs < 0)) {
Log_OC.w(
TAG,
"Skipping insertDBEntries: enabled=${folder.isEnabled}, " +
"exists=${folder.isExisting}, enabledTs=$enabledTimestampMs"
)
return
}

when (folder.type) {
MediaFolderType.IMAGE -> {
repository.insertFromUri(MediaStore.Images.Media.INTERNAL_CONTENT_URI, folder)
repository.insertFromUri(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, folder)
}

MediaFolderType.VIDEO -> {
repository.insertFromUri(MediaStore.Video.Media.INTERNAL_CONTENT_URI, folder)
repository.insertFromUri(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, folder)
}

else -> {
insertCustomFolderIntoDB(folder, repository)
}
}
}

/**
* Attempts to get the file path from a content URI string (e.g., content://media/external/images/media/2281)
* and checks its type. If the conditions are met, the file is stored for auto-upload.
* <p>
* If any attempt fails, the method returns {@code false}.
*
* @param syncedFolder The folder marked for auto-upload.
* @param contentUris An array of content URI strings collected from
* {@link ContentObserverWork##checkAndTriggerAutoUpload()}.
* @return {@code true} if all changed content URIs were successfully stored; {@code false} otherwise.
*/
fun insertChangedEntries(
syncedFolder: SyncedFolder,
contentUris: Array<String>?,
repository: FileSystemRepository
): Boolean {
contentUris?.forEach { uriString ->
try {
val uri = uriString.toUri()
repository.insertFromUri(uri, syncedFolder, true)
} catch (e: Exception) {
Log_OC.e(TAG, "Invalid URI: $uriString", e)
return false
}
}

Log_OC.d(TAG, "Changed content URIs successfully stored")

return true
}

fun insertCustomFolderIntoDB(folder: SyncedFolder, repository: FileSystemRepository): Int {
val path = Paths.get(folder.localPath)

if (!Files.exists(path)) {
Expand Down Expand Up @@ -70,20 +130,9 @@ class AutoUploadHelper {
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
)
repository.insertOrReplace(localPath, lastModified, creationTime, folder)

fileCount++

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.operations.UploadFileOperation
import com.owncloud.android.ui.activity.SettingsActivity
import com.owncloud.android.utils.FileStorageUtils
import com.owncloud.android.utils.FilesSyncHelper
import com.owncloud.android.utils.MimeType
import com.owncloud.android.utils.theme.ViewThemeUtils
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -230,16 +229,16 @@ class AutoUploadWorker(
private suspend fun collectFileChangesFromContentObserverWork(contentUris: Array<String>?) = try {
withContext(Dispatchers.IO) {
if (contentUris.isNullOrEmpty()) {
FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper)
helper.insertEntries(syncedFolder, repository)
} else {
val isContentUrisStored = FilesSyncHelper.insertChangedEntries(syncedFolder, contentUris)
val isContentUrisStored = helper.insertChangedEntries(syncedFolder, contentUris, repository)
if (!isContentUrisStored) {
Log_OC.w(
TAG,
"changed content uris not stored, fallback to insert all db entries to not lose files"
)

FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper)
helper.insertEntries(syncedFolder, repository)
}
}
syncedFolder.lastScanTimestampMs = System.currentTimeMillis()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,27 @@

package com.nextcloud.client.jobs.autoUpload

import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import com.nextcloud.client.database.dao.FileSystemDao
import com.nextcloud.client.database.entity.FilesystemEntity
import com.nextcloud.utils.extensions.shouldSkipFile
import com.nextcloud.utils.extensions.toFile
import com.owncloud.android.datamodel.SyncedFolder
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.utils.SyncedFolderUtils
import java.io.File
import java.util.zip.CRC32

class FileSystemRepository(private val dao: FileSystemDao) {
@Suppress("TooGenericExceptionCaught", "NestedBlockDepth", "MagicNumber", "ReturnCount")
class FileSystemRepository(private val dao: FileSystemDao, private val context: Context) {

companion object {
private const val TAG = "FilesystemRepository"
const val BATCH_SIZE = 50
}

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

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

Expand All @@ -63,4 +69,140 @@ class FileSystemRepository(private val dao: FileSystemDao) {
Log_OC.e(TAG, "Error marking file as uploaded: ${e.message}", e)
}
}

@JvmOverloads
fun insertFromUri(uri: Uri, syncedFolder: SyncedFolder, checkFileType: Boolean = false) {
val projection = arrayOf(
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.MediaColumns.DATE_ADDED
)

var syncedPath = syncedFolder.localPath
if (syncedPath.isNullOrEmpty()) {
Log_OC.w(TAG, "Synced folder path is null or empty")
return
}

if (!syncedPath.endsWith(File.separator)) {
syncedPath += File.separator
}

val selection = "${MediaStore.MediaColumns.DATA} LIKE ?"
val selectionArgs = arrayOf("$syncedPath%")

Log_OC.d(TAG, "Querying MediaStore for files in: $syncedPath, uri: $uri")

val cursor = context.contentResolver.query(
uri,
projection,
selection,
selectionArgs,
null
)

cursor?.use {
val idxData = cursor.getColumnIndex(MediaStore.MediaColumns.DATA)
val idxModified = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED)
val idxAdded = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED)

if (idxData == -1) {
Log_OC.e(TAG, "MediaStore column DATA missing — cannot process URI: $uri")
return
}

while (cursor.moveToNext()) {
val filePath = cursor.getString(idxData)

val lastModifiedMs = if (idxModified != -1) {
cursor.getLong(idxModified) * 1000
} else {
null
}

val creationTimeMs = if (idxAdded != -1) {
cursor.getLong(idxAdded) * 1000
} else {
null
}

Log_OC.d(
TAG,
"Found file: $filePath (created=$creationTimeMs, modified=$lastModifiedMs)"
)

insertOrReplace(filePath, lastModifiedMs, creationTimeMs, syncedFolder, checkFileType)
}
}
}

fun insertOrReplace(
localPath: String?,
lastModified: Long?,
creationTime: Long?,
syncedFolder: SyncedFolder,
checkFileType: Boolean = false
) {
try {
val file = localPath?.toFile()
if (file == null) {
Log_OC.w(TAG, "file null, cannot insert or replace: $localPath")
return
}

if (checkFileType && !syncedFolder.containsTypedFile(file, localPath)) {
Log_OC.w(TAG, "synced folder not contains typed file: $localPath")
return
}

val fileModified = (lastModified ?: file.lastModified())
val shouldSkipFileBasedOnFolderSettings = syncedFolder.shouldSkipFile(file, fileModified, creationTime)
if (shouldSkipFileBasedOnFolderSettings) {
return
}

val entity = dao.getFileByPathAndFolder(localPath, syncedFolder.id.toString())
if (entity != null && entity.fileSentForUpload == 1) {
Log_OC.w(
TAG,
"file already uploaded path: $localPath, " +
"syncedFolder: ${syncedFolder.localPath}, ${syncedFolder.id}"
)
return
}

val crc = getFileChecksum(file)

val newEntity = FilesystemEntity(
id = entity?.id,
localPath = localPath,
fileIsFolder = if (file.isDirectory) 1 else 0,
fileFoundRecently = System.currentTimeMillis(),
fileSentForUpload = 0,
syncedFolderId = syncedFolder.id.toString(),
crc32 = crc?.toString(),
fileModified = fileModified
)

Log_OC.d(TAG, "inserting new file system entity: $newEntity")

dao.insertOrReplace(newEntity)
} catch (e: Exception) {
Log_OC.e(TAG, "Failed to insert/update file: $localPath", e)
}
}

private fun getFileChecksum(file: File): Long? = try {
file.inputStream().use { fis ->
val crc = CRC32()
val buffer = ByteArray(64 * 1024)
var bytesRead: Int
while (fis.read(buffer).also { bytesRead = it } > 0) {
crc.update(buffer, 0, bytesRead)
}
crc.value
}
} catch (_: Exception) {
null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ private const val TAG = "SyncedFolderExtensions"
*/
@Suppress("ReturnCount")
fun SyncedFolder.shouldSkipFile(file: File, lastModified: Long, creationTime: Long?): Boolean {
Log_OC.d(TAG, "Checking file: ${file.name}, lastModified=$lastModified, lastScan=$lastScanTimestampMs")

if (isExcludeHidden && file.isHidden) {
Log_OC.d(TAG, "Skipping hidden: ${file.absolutePath}")
return true
Expand Down
Loading
Loading