From 79c8d28d8a9792aa626e94da294fd4b4de6f7905 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 7 Jul 2026 15:57:26 +0200 Subject: [PATCH 1/2] unify usage of isSameFileOnRemote Signed-off-by: alperozturk96 --- .../client/jobs/upload/FileUploadHelper.kt | 97 +++++++++++++++---- .../utils/UploadErrorNotificationManager.kt | 7 +- .../utils/extensions/RemoteFileExtensions.kt | 35 ------- .../datamodel/UploadsStorageManager.kt | 7 +- .../operations/UploadFileOperation.java | 7 +- .../helper/UploadListAdapterActionHandler.kt | 15 ++- 6 files changed, 92 insertions(+), 76 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index 816c81898b24..50c9e1cb7c9c 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -23,11 +23,14 @@ import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.network.Connectivity import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.notifications.AppWideNotificationManager +import com.nextcloud.utils.TimeConstants import com.nextcloud.utils.extensions.checkWCFRestrictions +import com.nextcloud.utils.extensions.getBitmapSize +import com.nextcloud.utils.extensions.getExifSize import com.nextcloud.utils.extensions.getUploadIds import com.nextcloud.utils.extensions.isAnonymous import com.nextcloud.utils.extensions.isLastResultConflictError -import com.nextcloud.utils.extensions.isSame +import com.nextcloud.utils.extensions.toFile import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager @@ -40,7 +43,6 @@ import com.owncloud.android.files.services.NameCollisionPolicy import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.OwnCloudClientFactory import com.owncloud.android.lib.common.network.OnDatatransferProgressListener -import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation import com.owncloud.android.lib.resources.files.model.RemoteFile @@ -51,6 +53,8 @@ import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.ui.adapter.uploadList.helper.ConflictHandlingResult import com.owncloud.android.ui.adapter.uploadList.helper.UploadListAdapterActionHandler import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.FileUtil +import com.owncloud.android.utils.MimeTypeUtil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -185,21 +189,17 @@ class FileUploadHelper { val batteryStatus = powerManagementService.battery val uploadsToRetry = mutableListOf() - - val currentAccount = accountManager.currentAccount - val context = MainApp.getAppContext() - var ownCloudClient: OwnCloudClient? = null - if (!currentAccount.isAnonymous(context)) { - ownCloudClient = - OwnCloudClientFactory.createOwnCloudClient(accountManager.currentAccount, MainApp.getAppContext()) - } + val ownCloudClient = createClientForCurrentAccount(accountManager) val uploadActionHandler = UploadListAdapterActionHandler() for (upload in uploads) { if (upload.isLastResultConflictError()) { ownCloudClient?.let { - conflictHandlingResult = - uploadActionHandler.handleConflict(upload, ownCloudClient, uploadsStorageManager) + conflictHandlingResult = uploadActionHandler.handleConflict( + upload, + ownCloudClient, + uploadsStorageManager + ) } continue } @@ -249,6 +249,16 @@ class FileUploadHelper { return@withContext showNotExistMessage } + private fun createClientForCurrentAccount(accountManager: UserAccountManager): OwnCloudClient? { + val context = MainApp.getAppContext() + val currentAccount = accountManager.currentAccount + return if (currentAccount.isAnonymous(context)) { + null + } else { + OwnCloudClientFactory.createOwnCloudClient(currentAccount, context) + } + } + @JvmOverloads @Suppress("LongParameterList") fun uploadNewFiles( @@ -578,22 +588,75 @@ class FileUploadHelper { } } - @Suppress("MagicNumber", "ReturnCount", "ComplexCondition") - fun isSameFileOnRemote(user: User?, localPath: String?, remotePath: String?, context: Context?): Boolean { - if (user == null || localPath == null || remotePath == null || context == null) { + @Suppress("ReturnCount") + fun isSameFileOnRemote(localPath: String?, remotePath: String?): Boolean { + if (localPath == null || remotePath == null) { Log_OC.e(TAG, "cannot compare remote and local file") return false } + Log_OC.d(TAG, "comparing local file against remote file") + + val client = createClientForCurrentAccount(accountManager) val operation = ReadFileRemoteOperation(remotePath) - val result: RemoteOperationResult<*> = operation.execute(user, context) + val result = operation.execute(client) if (result.isSuccess) { val remoteFile = result.data[0] as RemoteFile - return remoteFile.isSame(localPath) + val result = (remoteFile.isSame(localPath) || isETagUnchangedSinceLastSync(remoteFile, remotePath)) + if (result) { + Log_OC.d(TAG, "local file and remote file are same") + } else { + Log_OC.d(TAG, "local file different than remote file") + } + return result } + + Log_OC.d(TAG, "local file different than remote file") + return false } + private fun isETagUnchangedSinceLastSync( + remoteFile: RemoteFile, + remotePath: String, + ): Boolean { + val knownEtag = fileStorageManager.getFileByEncryptedRemotePath(remotePath)?.etag + return !knownEtag.isNullOrEmpty() && knownEtag == remoteFile.etag + } + + private fun RemoteFile.isSame(path: String?): Boolean { + val localFile = path?.toFile() ?: return false + + // remote file timestamp in millisecond not microsecond + val localLastModifiedTimestamp = localFile.lastModified() / TimeConstants.MILLIS_PER_SECOND + val localCreationTimestamp = FileUtil.getCreationTimestamp(localFile) + val localSize: Long = localFile.length() + + return size == localSize && + localCreationTimestamp != null && + localCreationTimestamp == creationTimestamp && + modifiedTimestamp == localLastModifiedTimestamp * TimeConstants.MILLIS_PER_SECOND && + this.areImageDimensionsSame(path) + } + + @Suppress("ReturnCount") + private fun RemoteFile.areImageDimensionsSame(path: String): Boolean { + if (!MimeTypeUtil.isImage(mimeType)) { + // can't compare it's not image + return true + } + + val localFileImageDimension = path.getExifSize() ?: path.getBitmapSize() + if (localFileImageDimension == null) { + // can't compare local file image dimension is not determined + return true + } + + return localFileImageDimension.first.toFloat() == imageDimension?.width && + localFileImageDimension.second.toFloat() == imageDimension?.height + } + + fun showFileUploadLimitMessage(activity: Activity) { val message = activity.resources.getQuantityString( R.plurals.file_upload_limit_message, diff --git a/app/src/main/java/com/nextcloud/client/jobs/utils/UploadErrorNotificationManager.kt b/app/src/main/java/com/nextcloud/client/jobs/utils/UploadErrorNotificationManager.kt index 5a6b142f8724..33a606093597 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/utils/UploadErrorNotificationManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/utils/UploadErrorNotificationManager.kt @@ -80,12 +80,7 @@ object UploadErrorNotificationManager { // do not show an error notification when uploading the same file again if (result.code.isConflict()) { val isSameFile = withContext(Dispatchers.IO) { - FileUploadHelper.instance().isSameFileOnRemote( - operation.user, - operation.storagePath, - operation.remotePath, - context - ) + FileUploadHelper.instance().isSameFileOnRemote(operation.storagePath, operation.remotePath) } if (isSameFile) { diff --git a/app/src/main/java/com/nextcloud/utils/extensions/RemoteFileExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/RemoteFileExtensions.kt index 22758d6140a2..7d48208022f7 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/RemoteFileExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/RemoteFileExtensions.kt @@ -7,27 +7,9 @@ package com.nextcloud.utils.extensions -import com.nextcloud.utils.TimeConstants import com.owncloud.android.lib.resources.files.model.RemoteFile import com.owncloud.android.lib.resources.shares.ShareeUser import com.owncloud.android.lib.resources.tags.Tag -import com.owncloud.android.utils.FileUtil -import com.owncloud.android.utils.MimeTypeUtil - -fun RemoteFile.isSame(path: String?): Boolean { - val localFile = path?.toFile() ?: return false - - // remote file timestamp in millisecond not microsecond - val localLastModifiedTimestamp = localFile.lastModified() / TimeConstants.MILLIS_PER_SECOND - val localCreationTimestamp = FileUtil.getCreationTimestamp(localFile) - val localSize: Long = localFile.length() - - return size == localSize && - localCreationTimestamp != null && - localCreationTimestamp == creationTimestamp && - modifiedTimestamp == localLastModifiedTimestamp * TimeConstants.MILLIS_PER_SECOND && - this.areImageDimensionsSame(path) -} fun RemoteFile.sharedViaLink(): Boolean = sharees?.any { it.shareType?.isLink == true } ?: false @@ -36,20 +18,3 @@ fun RemoteFile.sharedWithSharee(): Boolean = sharees?.isNotEmpty() ?: false fun RemoteFile.getShareeList(): List = sharees?.toList() ?: emptyList() fun RemoteFile.tags(): List = tags?.mapNotNull { it } ?: emptyList() - -@Suppress("ReturnCount") -private fun RemoteFile.areImageDimensionsSame(path: String): Boolean { - if (!MimeTypeUtil.isImage(mimeType)) { - // can't compare it's not image - return true - } - - val localFileImageDimension = path.getExifSize() ?: path.getBitmapSize() - if (localFileImageDimension == null) { - // can't compare local file image dimension is not determined - return true - } - - return localFileImageDimension.first.toFloat() == imageDimension?.width && - localFileImageDimension.second.toFloat() == imageDimension?.height -} diff --git a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt index e294114fb003..47084496a6b2 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt @@ -495,12 +495,7 @@ class UploadsStorageManager( status = UploadStatus.UPLOAD_SUCCEEDED result = UploadResult.UPLOADED } else if (code.isConflict()) { - val isSame = FileUploadHelper().isSameFileOnRemote( - upload.user, - upload.storagePath, - upload.remotePath, - upload.context - ) + val isSame = FileUploadHelper().isSameFileOnRemote(upload.storagePath, upload.remotePath) if (isSame) { result = UploadResult.SAME_FILE_CONFLICT diff --git a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java index bb11eb490dda..67e7f25852f7 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -53,7 +53,6 @@ import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation; import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation; import com.owncloud.android.lib.resources.files.model.RemoteFile; -import com.owncloud.android.lib.resources.status.E2EVersion; import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.operations.common.SyncOperation; import com.owncloud.android.operations.e2e.E2EClientData; @@ -1309,14 +1308,14 @@ private RemoteOperationResult checkNameCollision(OCFile parentFile, boolean isSameFileOnRemote = false; if (mFile != null) { isSameFileOnRemote = FileUploadHelper.Companion.instance() - .isSameFileOnRemote(user, mFile.getStoragePath(), mRemotePath, mContext); + .isSameFileOnRemote(mFile.getStoragePath(), mRemotePath); } if (isSameFileOnRemote) { return new RemoteOperationResult<>(ResultCode.OK); - } else { - return new RemoteOperationResult<>(ResultCode.SYNC_CONFLICT); } + + return new RemoteOperationResult<>(ResultCode.SYNC_CONFLICT); } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterActionHandler.kt b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterActionHandler.kt index 66e0923d1684..ad19e82bf6ac 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterActionHandler.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterActionHandler.kt @@ -7,7 +7,7 @@ package com.owncloud.android.ui.adapter.uploadList.helper -import com.nextcloud.utils.extensions.isSame +import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.utils.extensions.updateStatus import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.db.OCUpload @@ -46,20 +46,19 @@ class UploadListAdapterActionHandler : UploadListAdapterAction { } } + if (FileUploadHelper.instance().isSameFileOnRemote(upload.localPath, upload.remotePath)) { + updateStatus(upload, storageManager, success = true) + return@withContext ConflictHandlingResult.ConflictNotExistsSameFile + } + val remoteFile = operationResult.data[0] as? RemoteFile ?: run { Log_OC.e(TAG, "cannot check conflict, operation result cannot be cast to RemoteFile") return@withContext ConflictHandlingResult.CannotCheckConflict } - val ocFile = FileStorageUtils.fillOCFile(remoteFile) - if (remoteFile.isSame(ocFile.storagePath)) { - updateStatus(upload, storageManager, success = true) - ConflictHandlingResult.ConflictNotExistsSameFile - } else { - ConflictHandlingResult.ShowConflictResolveDialog(ocFile, upload) - } + ConflictHandlingResult.ShowConflictResolveDialog(ocFile, upload) } private fun updateStatus(upload: OCUpload, storageManager: UploadsStorageManager, success: Boolean) { From 1a5365d43487fdc0922ce345a2b41be5a48f1218 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 7 Jul 2026 16:17:05 +0200 Subject: [PATCH 2/2] add etag for oc upload Signed-off-by: alperozturk96 --- .../101.json | 1313 +++++++++++++++++ .../com/nextcloud/utils/UploadDateTests.kt | 3 +- .../android/utils/FileUploadHelperTest.kt | 6 +- .../client/database/NextcloudDatabase.kt | 3 +- .../client/database/entity/UploadEntity.kt | 8 +- .../client/jobs/upload/FileUploadHelper.kt | 18 +- .../datamodel/UploadsStorageManager.kt | 26 + .../com/owncloud/android/db/OCUpload.java | 18 + .../com/owncloud/android/db/ProviderMeta.java | 3 +- .../operations/UploadFileOperation.java | 5 + 10 files changed, 1388 insertions(+), 15 deletions(-) create mode 100644 app/schemas/com.nextcloud.client.database.NextcloudDatabase/101.json diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/101.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/101.json new file mode 100644 index 000000000000..a7fb9c2cec40 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/101.json @@ -0,0 +1,1313 @@ +{ + "formatVersion": 1, + "database": { + "version": 101, + "identityHash": "af2c9fa98b3c1c82fa3c139f1ac7b156", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` TEXT, `forbidden_filenames` TEXT, `forbidden_filename_extensions` TEXT, `forbidden_filename_basenames` TEXT, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT, `default_permissions` INTEGER, `user_status_supports_busy` INTEGER, `windows_compatible_filenames` INTEGER, `has_valid_subscription` INTEGER, `client_integration_json` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT" + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT" + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER" + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER" + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT" + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT" + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT" + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT" + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT" + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT" + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT" + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT" + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER" + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER" + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "TEXT" + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "TEXT" + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "TEXT" + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "TEXT" + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER" + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT" + }, + { + "fieldPath": "defaultPermissions", + "columnName": "default_permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsBusy", + "columnName": "user_status_supports_busy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWCFEnabled", + "columnName": "windows_compatible_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasValidSubscription", + "columnName": "has_valid_subscription", + "affinity": "INTEGER" + }, + { + "fieldPath": "clientIntegrationJson", + "columnName": "client_integration_json", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT, `uploaded` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT" + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT" + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER" + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER" + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT" + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER" + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT" + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER" + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER" + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER" + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER" + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER" + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT" + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT" + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT" + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT" + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT" + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT" + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER" + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT" + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT" + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER, `attributes` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER" + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT" + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT" + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT" + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT" + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER" + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER" + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `upload_end_timestamp_long` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT, `etag` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER" + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestampLong", + "columnName": "upload_end_timestamp_long", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER" + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT" + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "recommended_files", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `directory` TEXT NOT NULL, `extension` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `has_preview` INTEGER NOT NULL, `reason` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `account_name` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directory", + "columnName": "directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extension", + "columnName": "extension", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reason", + "columnName": "reason", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "assistant", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT, `type` TEXT, `status` TEXT, `userId` TEXT, `appId` TEXT, `input` TEXT, `output` TEXT, `completionExpectedAt` INTEGER, `progress` INTEGER, `lastUpdated` INTEGER, `scheduledAt` INTEGER, `endedAt` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "appId", + "columnName": "appId", + "affinity": "TEXT" + }, + { + "fieldPath": "input", + "columnName": "input", + "affinity": "TEXT" + }, + { + "fieldPath": "output", + "columnName": "output", + "affinity": "TEXT" + }, + { + "fieldPath": "completionExpectedAt", + "columnName": "completionExpectedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER" + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "endedAt", + "columnName": "endedAt", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'af2c9fa98b3c1c82fa3c139f1ac7b156')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/nextcloud/utils/UploadDateTests.kt b/app/src/androidTest/java/com/nextcloud/utils/UploadDateTests.kt index 795148f2b3e5..e8ea2b14b09a 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/UploadDateTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/UploadDateTests.kt @@ -62,7 +62,8 @@ class UploadDateTests { isWhileChargingOnly = 1, isWifiOnly = 1, createdBy = 5, - folderUnlockToken = "token123" + folderUnlockToken = "token123", + etag = null ) val upload = entity.toOCUpload() diff --git a/app/src/androidTest/java/com/owncloud/android/utils/FileUploadHelperTest.kt b/app/src/androidTest/java/com/owncloud/android/utils/FileUploadHelperTest.kt index 502eb923526b..dc23118b35a4 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/FileUploadHelperTest.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/FileUploadHelperTest.kt @@ -81,7 +81,8 @@ class FileUploadHelperTest { isWhileChargingOnly = isWhileChargingOnly, isWifiOnly = isWifiOnly, createdBy = createdBy, - folderUnlockToken = folderUnlockToken + folderUnlockToken = folderUnlockToken, + etag = null ) private fun buildOCUpload( @@ -406,7 +407,8 @@ class FileUploadHelperTest { isWhileChargingOnly = null, isWifiOnly = null, createdBy = null, - folderUnlockToken = null + folderUnlockToken = null, + etag = null ) // should not throw; optional fields simply stay at OCUpload defaults diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index 39fa5c7b33ae..9dea5848bfab 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -97,8 +97,9 @@ import com.owncloud.android.db.ProviderMeta AutoMigration(from = 95, to = 96), AutoMigration(from = 96, to = 97, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), // manual migration used for 97 to 98 - AutoMigration(from = 98, to = 99) + AutoMigration(from = 98, to = 99), // manual migration used for 99 to 100 + AutoMigration(from = 100, to = 101) ], exportSchema = true ) diff --git a/app/src/main/java/com/nextcloud/client/database/entity/UploadEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/UploadEntity.kt index ff4ff9e91c6f..4068a5c9f31e 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/UploadEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/UploadEntity.kt @@ -60,7 +60,9 @@ data class UploadEntity( @ColumnInfo(name = ProviderTableMeta.UPLOADS_CREATED_BY) val createdBy: Int?, @ColumnInfo(name = ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN) - val folderUnlockToken: String? + val folderUnlockToken: String?, + @ColumnInfo(name = ProviderTableMeta.UPLOADS_ETAG) + val etag: String? ) fun UploadEntity.toOCUpload(capability: OCCapability? = null): OCUpload? { @@ -88,6 +90,7 @@ fun UploadEntity.toOCUpload(capability: OCCapability? = null): OCUpload? { isWifiOnly?.let { upload.isUseWifiOnly = it == 1 } isWhileChargingOnly?.let { upload.isWhileChargingOnly = it == 1 } folderUnlockToken?.let { upload.folderUnlockToken = it } + etag?.let { upload.etag = it } return upload } @@ -129,6 +132,7 @@ fun OCUpload.toUploadEntity(): UploadEntity { isWifiOnly = if (isUseWifiOnly) 1 else 0, isWhileChargingOnly = if (isWhileChargingOnly) 1 else 0, folderUnlockToken = folderUnlockToken, - uploadTime = null + uploadTime = null, + etag = etag ) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index 50c9e1cb7c9c..3e4f531c3fb8 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -602,7 +602,8 @@ class FileUploadHelper { val result = operation.execute(client) if (result.isSuccess) { val remoteFile = result.data[0] as RemoteFile - val result = (remoteFile.isSame(localPath) || isETagUnchangedSinceLastSync(remoteFile, remotePath)) + val result = remoteFile.isSame(localPath) || + isETagUnchangedSinceLastUpload(remoteFile, localPath, remotePath) if (result) { Log_OC.d(TAG, "local file and remote file are same") } else { @@ -616,12 +617,14 @@ class FileUploadHelper { return false } - private fun isETagUnchangedSinceLastSync( - remoteFile: RemoteFile, - remotePath: String, - ): Boolean { - val knownEtag = fileStorageManager.getFileByEncryptedRemotePath(remotePath)?.etag - return !knownEtag.isNullOrEmpty() && knownEtag == remoteFile.etag + private fun isETagUnchangedSinceLastUpload(remoteFile: RemoteFile, localPath: String, remotePath: String): Boolean { + val accountName = accountManager.user.accountName + val lastUploadedEtag = getUploadByPaths(accountName, localPath, remotePath)?.etag + val isUnchanged = !lastUploadedEtag.isNullOrEmpty() && lastUploadedEtag == remoteFile.etag + if (isUnchanged) { + Log_OC.d(TAG, "remote file is the version this client uploaded") + } + return isUnchanged } private fun RemoteFile.isSame(path: String?): Boolean { @@ -656,7 +659,6 @@ class FileUploadHelper { localFileImageDimension.second.toFloat() == imageDimension?.height } - fun showFileUploadLimitMessage(activity: Activity) { val message = activity.resources.getQuantityString( R.plurals.file_upload_limit_message, diff --git a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt index 47084496a6b2..72e222c9132e 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt @@ -96,6 +96,7 @@ class UploadsStorageManager( put(ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP_LONG, uploadEndTimestamp) put(ProviderTableMeta.UPLOADS_FILE_SIZE, ocUpload.fileSize) put(ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN, ocUpload.folderUnlockToken) + put(ProviderTableMeta.UPLOADS_ETAG, ocUpload.etag) } val result = contentResolver.update( @@ -116,6 +117,26 @@ class UploadsStorageManager( return result } + fun updateUploadEtag(ocUpload: OCUpload) { + val etag = ocUpload.etag + if (etag.isNullOrEmpty()) { + return + } + + val cv = ContentValues().apply { + put(ProviderTableMeta.UPLOADS_ETAG, etag) + } + + val result = contentResolver.update( + ProviderTableMeta.CONTENT_URI_UPLOADS, + cv, + ProviderTableMeta._ID + "=? AND " + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "=?", + arrayOf(ocUpload.uploadId.toString(), ocUpload.accountName) + ) + + Log_OC.d(TAG, "updateUploadEtag returns with: $result for file: ${ocUpload.localPath}") + } + private fun updateUploadInternal( c: Cursor, status: UploadStatus?, @@ -404,6 +425,11 @@ class UploadsStorageManager( isUseWifiOnly = c.int(ProviderTableMeta.UPLOADS_IS_WIFI_ONLY) == 1 isWhileChargingOnly = c.int(ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY) == 1 folderUnlockToken = c.str(ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN) + + val etagIndex = c.getColumnIndex(ProviderTableMeta.UPLOADS_ETAG) + if (etagIndex > -1) { + etag = c.getString(etagIndex) + } } } diff --git a/app/src/main/java/com/owncloud/android/db/OCUpload.java b/app/src/main/java/com/owncloud/android/db/OCUpload.java index 6632fcf12537..10b1da816727 100644 --- a/app/src/main/java/com/owncloud/android/db/OCUpload.java +++ b/app/src/main/java/com/owncloud/android/db/OCUpload.java @@ -114,6 +114,13 @@ public class OCUpload implements Parcelable { */ private String folderUnlockToken; + /** + * Server etag of the file created by the last successful upload of this record. + * Written only by the upload machinery; unlike the etag in the files table it is + * never refreshed by browse/search/sync operations. + */ + private String etag; + /** * temporary values, used for sorting */ @@ -175,6 +182,7 @@ private void resetData() { useWifiOnly = true; whileChargingOnly = false; folderUnlockToken = ""; + etag = null; } public void setDataFixed(FileUploadHelper uploadHelper) { @@ -286,6 +294,7 @@ private void readFromParcel(Parcel source) { useWifiOnly = source.readInt() == 1; whileChargingOnly = source.readInt() == 1; folderUnlockToken = source.readString(); + etag = source.readString(); } @Override @@ -335,6 +344,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeInt(useWifiOnly ? 1 : 0); dest.writeInt(whileChargingOnly ? 1 : 0); dest.writeString(folderUnlockToken); + dest.writeString(etag); } public long getUploadId() { @@ -448,4 +458,12 @@ public void setWhileChargingOnly(boolean whileChargingOnly) { public void setFolderUnlockToken(String folderUnlockToken) { this.folderUnlockToken = folderUnlockToken; } + + public String getEtag() { + return this.etag; + } + + public void setEtag(String etag) { + this.etag = etag; + } } diff --git a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java index e12b0b44ec54..7a3158df4190 100644 --- a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -23,7 +23,7 @@ */ public class ProviderMeta { public static final String DB_NAME = "filelist"; - public static final int DB_VERSION = 100; + public static final int DB_VERSION = 101; private ProviderMeta() { // No instance @@ -313,6 +313,7 @@ static public class ProviderTableMeta implements BaseColumns { public static final String UPLOADS_IS_WHILE_CHARGING_ONLY = "is_while_charging_only"; public static final String UPLOADS_IS_WIFI_ONLY = "is_wifi_only"; public static final String UPLOADS_FOLDER_UNLOCK_TOKEN = "folder_unlock_token"; + public static final String UPLOADS_ETAG = "etag"; // Columns of offline operation table public static final String OFFLINE_OPERATION_PARENT_OC_FILE_ID = "offline_operations_parent_oc_file_id"; diff --git a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java index 67e7f25852f7..5a863e488069 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -1746,6 +1746,11 @@ private void saveUploadedFile(OwnCloudClient client) { if (result.isSuccess()) { updateOCFile(file, (RemoteFile) result.getData().get(0)); file.setLastSyncDateForProperties(syncDate); + + // remember which server version this upload produced, so later collision checks + // can tell "remote is still our own upload" apart from a real conflict + mUpload.setEtag(file.getEtag()); + uploadsStorageManager.updateUploadEtag(mUpload); } else { Log_OC.e(TAG, "Error reading properties of file after successful upload; this is gonna hurt..."); }