From f84e66f9f80cf4ad1bd6e1e375d88f9e035d6de8 Mon Sep 17 00:00:00 2001 From: zerox80 Date: Thu, 4 Jun 2026 14:05:25 +0200 Subject: [PATCH 01/13] Add original checksums for TUS uploads --- .../workers/ContentUriUploadCacheValidator.kt | 27 ++++ .../android/workers/TusUploadHelper.kt | 73 +++++---- .../workers/UploadFileFromContentUriWorker.kt | 135 ++++++++++++++-- .../workers/UploadFileFromFileSystemWorker.kt | 34 +++- .../ContentUriUploadCacheValidatorTest.kt | 88 ++++++++++ .../android/workers/TusUploadHelperTest.kt | 92 ++++++++++- .../lib/common/http/HttpConstants.java | 2 + .../tus/PatchTusUploadChunkRemoteOperation.kt | 12 ++ .../resources/files/tus/TusChecksumHelper.kt | 150 ++++++++++++++++++ .../files/tus/TusChecksumHelperTest.kt | 93 +++++++++++ .../resources/files/tus/TusIntegrationTest.kt | 92 +++++++++++ .../datasources/LocalTransferDataSource.kt | 2 + .../OCLocalTransferDataSource.kt | 4 + .../android/data/transfers/db/TransferDao.kt | 9 ++ .../repository/OCTransferRepository.kt | 3 + .../repository/OCTransferRepositoryTest.kt | 11 ++ .../domain/transfers/TransferRepository.kt | 2 + 17 files changed, 778 insertions(+), 51 deletions(-) create mode 100644 opencloudApp/src/main/java/eu/opencloud/android/workers/ContentUriUploadCacheValidator.kt create mode 100644 opencloudApp/src/test/java/eu/opencloud/android/workers/ContentUriUploadCacheValidatorTest.kt create mode 100644 opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/TusChecksumHelper.kt create mode 100644 opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusChecksumHelperTest.kt diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/ContentUriUploadCacheValidator.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/ContentUriUploadCacheValidator.kt new file mode 100644 index 0000000000..3ffbf1cadc --- /dev/null +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/ContentUriUploadCacheValidator.kt @@ -0,0 +1,27 @@ +/** + * openCloud Android client application + * + * Copyright (C) 2026 OpenCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package eu.opencloud.android.workers + +internal object ContentUriUploadCacheValidator { + fun isValidCacheSize( + actualSize: Long, + expectedSize: Long, + ): Boolean = + actualSize > 0 && (expectedSize <= 0 || actualSize == expectedSize) +} diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt index 79a0327f1e..a0e77f5145 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt @@ -5,12 +5,14 @@ import eu.opencloud.android.domain.capabilities.model.OCCapability import eu.opencloud.android.domain.transfers.TransferRepository import eu.opencloud.android.domain.transfers.model.OCTransfer import eu.opencloud.android.lib.common.OpenCloudClient +import eu.opencloud.android.lib.common.http.HttpConstants import eu.opencloud.android.lib.common.network.OnDatatransferProgressListener import eu.opencloud.android.lib.resources.files.chunks.ChunkedUploadFromFileSystemOperation import eu.opencloud.android.lib.resources.files.tus.CreateTusUploadRemoteOperation import eu.opencloud.android.lib.resources.files.tus.GetTusUploadOffsetRemoteOperation import eu.opencloud.android.lib.resources.files.tus.PatchTusUploadChunkRemoteOperation +import eu.opencloud.android.lib.resources.files.tus.TusChecksumHelper import eu.opencloud.android.domain.exceptions.FileNotFoundException import timber.log.Timber import java.io.File @@ -53,6 +55,8 @@ class TusUploadHelper( ) : String? { // Reset cancelled state for new upload cancelled = false + val checksum = TusChecksumHelper.parseStoredChecksum(transfer.tusUploadChecksum) + ?.takeIf { it.uploadAlgorithm == TusChecksumHelper.SHA1_WIRE_ALGORITHM } Timber.d("TUS: starting upload for %s size=%d", remotePath, fileSize) val (resolvedTusUrl, createdOffset) = prepareUpload( @@ -64,7 +68,8 @@ class TusUploadHelper( fileSize = fileSize, mimeType = mimeType, lastModified = lastModified, - spaceWebDavUrl = spaceWebDavUrl + spaceWebDavUrl = spaceWebDavUrl, + checksum = checksum, ) val offset = fetchCurrentOffset(client, resolvedTusUrl, createdOffset) @@ -81,6 +86,7 @@ class TusUploadHelper( progressCallback = progressCallback, initialOffset = offset, uploadId = uploadId, + checksum = checksum, ) verifyUploadCompletion(finalOffset, fileSize, uploadId) @@ -97,10 +103,10 @@ class TusUploadHelper( fileSize: Long, mimeType: String, lastModified: String?, - spaceWebDavUrl: String? + spaceWebDavUrl: String?, + checksum: TusChecksumHelper.StoredChecksum?, ): Pair { var tusUrl = transfer.tusUploadUrl - val checksumHex = transfer.tusUploadChecksum?.substringAfter("sha256:") var createdOffset: Long? = null if (tusUrl.isNullOrBlank()) { @@ -110,7 +116,7 @@ class TusUploadHelper( "mimetype" to mimeType, ) lastModified?.takeIf { it.isNotBlank() }?.let { metadata["mtime"] = it } - checksumHex?.let { metadata["checksum"] = "sha256 $it" } + checksum?.let { metadata["checksum"] = it.metadataValue } Timber.d( "TUS: creating upload resource filename=%s size=%d metadata=%s", @@ -124,15 +130,20 @@ class TusUploadHelper( spaceWebDavUrl = spaceWebDavUrl ) - // Use creation-with-upload like the browser does for OpenCloud compatibility - val firstChunkSize = minOf(CreateTusUploadRemoteOperation.DEFAULT_FIRST_CHUNK, fileSize) + // Checked uploads must send every byte via PATCH so each chunk can carry Upload-Checksum. + val useCreationWithUpload = checksum == null + val firstChunkSize = if (useCreationWithUpload) { + minOf(CreateTusUploadRemoteOperation.DEFAULT_FIRST_CHUNK, fileSize) + } else { + null + } val creationResult = executeRemoteOperation { CreateTusUploadRemoteOperation( file = File(localPath), remotePath = remotePath, mimetype = mimeType, metadata = metadata, - useCreationWithUpload = true, + useCreationWithUpload = useCreationWithUpload, firstChunkSize = firstChunkSize, tusUrl = "", collectionUrlOverride = collectionUrl, @@ -152,7 +163,7 @@ class TusUploadHelper( tusUploadUrl = tusUrl, tusUploadLength = fileSize, tusUploadMetadata = metadataString, - tusUploadChecksum = checksumHex?.let { "sha256:$it" }, + tusUploadChecksum = checksum?.storageValue, tusResumableVersion = "1.0.0", tusUploadExpires = null, tusUploadConcat = null, @@ -188,16 +199,7 @@ class TusUploadHelper( Timber.e("TUS: upload loop exited but offset=%d != fileSize=%d", offset, fileSize) throw java.io.IOException("TUS: upload incomplete - offset $offset does not match file size $fileSize") } - transferRepository.updateTusState( - id = uploadId, - tusUploadUrl = null, - tusUploadLength = null, - tusUploadMetadata = null, - tusUploadChecksum = null, - tusResumableVersion = null, - tusUploadExpires = null, - tusUploadConcat = null, - ) + clearTusState(uploadId) } private fun finalizeEtag( @@ -241,6 +243,7 @@ class TusUploadHelper( progressCallback: ((Long, Long) -> Unit)?, initialOffset: Long, uploadId: Long, + checksum: TusChecksumHelper.StoredChecksum?, ): Pair { var offset = initialOffset var lastEtag: String? = null @@ -264,6 +267,7 @@ class TusUploadHelper( offset = offset, chunkSize = chunkSize, httpMethodOverride = httpOverride, + checksum = checksum, ).apply { progressListener?.let { addDataTransferProgressListener(it) } } @@ -272,6 +276,12 @@ class TusUploadHelper( val patchResult = patchOperation.execute(client) lastEtag = patchOperation.etag.takeIf { it.isNotBlank() } activePatchOperation = null + if (checksum != null && isChecksumFailure(patchResult.httpCode)) { + clearTusState(uploadId) + throw java.io.IOException( + "TUS: checksum upload rejected with HTTP ${patchResult.httpCode} at offset $offset" + ) + } if (!patchResult.isSuccess || patchResult.data == null || patchResult.data!! < offset) { consecutiveFailures++ Timber.w( @@ -352,6 +362,9 @@ class TusUploadHelper( return Pair(offset, lastEtag) } + private fun isChecksumFailure(httpCode: Int): Boolean = + httpCode == HttpConstants.HTTP_BAD_REQUEST || httpCode == HttpConstants.HTTP_CHECKSUM_MISMATCH + private fun resolveTusCollectionUrl( client: OpenCloudClient, spaceWebDavUrl: String?, @@ -402,22 +415,26 @@ class TusUploadHelper( throw e } catch (e: FileNotFoundException) { Timber.w(e, "TUS: upload not found on server (404), clearing state to restart") - transferRepository.updateTusState( - id = uploadId, - tusUploadUrl = null, - tusUploadLength = null, - tusUploadMetadata = null, - tusUploadChecksum = null, - tusResumableVersion = null, - tusUploadExpires = null, - tusUploadConcat = null, - ) + clearTusState(uploadId) throw java.io.IOException("TUS: upload session lost (404), forcing restart", e) } catch (recoverError: Throwable) { Timber.w(recoverError, "TUS: recover offset failed") null } + private fun clearTusState(uploadId: Long) { + transferRepository.updateTusState( + id = uploadId, + tusUploadUrl = null, + tusUploadLength = null, + tusUploadMetadata = null, + tusUploadChecksum = null, + tusResumableVersion = null, + tusUploadExpires = null, + tusUploadConcat = null, + ) + } + companion object { const val DEFAULT_CHUNK_SIZE = ChunkedUploadFromFileSystemOperation.CHUNK_SIZE diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt index 08eedd26e2..f5a636b9af 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt @@ -60,6 +60,7 @@ import eu.opencloud.android.lib.common.operations.RemoteOperationResult.ResultCo import eu.opencloud.android.lib.resources.files.CheckPathExistenceRemoteOperation import eu.opencloud.android.lib.resources.files.CreateRemoteFolderOperation import eu.opencloud.android.lib.resources.files.UploadFileFromFileSystemOperation +import eu.opencloud.android.lib.resources.files.tus.TusChecksumHelper import eu.opencloud.android.presentation.authentication.AccountUtils import eu.opencloud.android.utils.NotificationUtils import eu.opencloud.android.utils.UPLOAD_NOTIFICATION_CHANNEL_ID @@ -147,18 +148,37 @@ class UploadFileFromContentUriWorker( cachePath = localStorageProvider.getTemporalPath(account.name, ocTransfer.spaceId) + File.separator + flatCacheName - // Re-copy if the cache file is missing or empty. A previous run may have copied it - // and then had it removed (e.g. by removeCacheFile() at the end of a successful run - // that the OS killed before bookkeeping). Only the contentUri from worker params is - // authoritative. val cacheFile = File(cachePath) - if (!cacheFile.exists() || cacheFile.length() == 0L) { + if (!isCacheFileReadyForUpload(cacheFile)) { checkDocumentFileExists() checkPermissionsToReadDocumentAreGranted() copyFileToLocalStorage() } } + private fun isCacheFileReadyForUpload(cacheFile: File): Boolean { + if (!cacheFile.exists()) return false + + val cacheSize = cacheFile.length() + val isValidCacheSize = ContentUriUploadCacheValidator.isValidCacheSize( + actualSize = cacheSize, + expectedSize = ocTransfer.fileSize, + ) + if (isValidCacheSize) return true + + Timber.w( + "Cached upload file for %s has invalid size. expected=%d actual=%d. Deleting and recopying.", + contentUri, + ocTransfer.fileSize, + cacheSize, + ) + if (!cacheFile.delete()) { + Timber.w("Could not delete invalid cached upload file: %s", cacheFile.absolutePath) + } + clearTusState() + return false + } + private fun areParametersValid(): Boolean { val paramAccountName = workerParameters.inputData.getString(KEY_PARAM_ACCOUNT_NAME) val paramUploadPath = workerParameters.inputData.getString(KEY_PARAM_UPLOAD_PATH) @@ -208,11 +228,14 @@ class UploadFileFromContentUriWorker( private fun copyFileToLocalStorage() { val documentFile = DocumentFile.fromSingleUri(appContext, contentUri) val cacheFile = File(cachePath) + val partFile = File("$cachePath.part") val cacheDir = cacheFile.parentFile if (cacheDir != null && !cacheDir.exists()) { cacheDir.mkdirs() } - cacheFile.createNewFile() + if (partFile.exists() && !partFile.delete()) { + Timber.w("Could not delete stale partial cache file: %s", partFile.absolutePath) + } // openInputStream can return null if the content provider is unavailable or permissions were revoked. // Failing here avoids silently uploading a 0-byte file. @@ -221,20 +244,60 @@ class UploadFileFromContentUriWorker( Timber.e("Failed to open input stream for %s — content provider unavailable or permissions revoked", contentUri) throw LocalFileNotFoundException() } - val outputStream = FileOutputStream(cachePath) - inputStream.use { input -> - outputStream.use { output -> - input.copyTo(output) + val checksumResult = try { + inputStream.use { input -> + FileOutputStream(partFile).use { output -> + TusChecksumHelper.copyAndSha1Hex(input, output) + } } + } catch (throwable: Throwable) { + partFile.delete() + throw throwable } - // Guard against a truncated or empty copy (e.g. file deleted mid-read). - if (cacheFile.length() == 0L) { - Timber.e("Cache file is 0 bytes after copy from %s — source may have been deleted mid-read", contentUri) + val copiedSize = checksumResult.bytesCopied + if (!ContentUriUploadCacheValidator.isValidCacheSize(copiedSize, ocTransfer.fileSize)) { + Timber.e( + "Partial cache copy from %s. expected=%d actual=%d", + contentUri, + ocTransfer.fileSize, + copiedSize, + ) + partFile.delete() + clearTusState() + throw IOException( + "Cache copy size mismatch for $contentUri: " + + "expected ${ocTransfer.fileSize} bytes, copied $copiedSize bytes" + ) + } + + if (cacheFile.exists() && !cacheFile.delete()) { + partFile.delete() + throw IOException("Could not replace cached upload file: ${cacheFile.absolutePath}") + } + if (!partFile.renameTo(cacheFile)) { + partFile.delete() + throw IOException("Could not finalize cached upload file: ${cacheFile.absolutePath}") + } + + val finalSize = cacheFile.length() + if (!ContentUriUploadCacheValidator.isValidCacheSize(finalSize, ocTransfer.fileSize)) { + Timber.e( + "Invalid finalized cache copy from %s. expected=%d actual=%d", + contentUri, + ocTransfer.fileSize, + finalSize, + ) cacheFile.delete() - throw LocalFileNotFoundException() + clearTusState() + throw IOException( + "Final cache copy size mismatch for $contentUri: " + + "expected ${ocTransfer.fileSize} bytes, copied $finalSize bytes" + ) } + persistTusChecksum(checksumResult.sha1Hex) + transferRepository.updateTransferSourcePath(uploadIdInStorageManager, contentUri.toString()) transferRepository.updateTransferLocalPath(uploadIdInStorageManager, cachePath) @@ -315,11 +378,16 @@ class UploadFileFromContentUriWorker( ) if (shouldTryTus) { + if (hasPendingTusSession && !hasStoredSha1Checksum()) { + Timber.w("TUS session for %s has no original checksum. Clearing state and recreating.", uploadPath) + clearTusState() + } + ensureOriginalTusChecksum() Timber.d( "Attempting TUS upload (size=%d, threshold=%d, resume=%s)", fileSize, TusUploadHelper.DEFAULT_CHUNK_SIZE, - hasPendingTusSession + !ocTransfer.tusUploadUrl.isNullOrBlank() ) val tusSucceeded = try { tusUploadHelper.upload( @@ -406,6 +474,34 @@ class UploadFileFromContentUriWorker( cacheFile.delete() } + private fun ensureOriginalTusChecksum() { + if (hasStoredSha1Checksum()) return + + val inputStream = appContext.contentResolver.openInputStream(contentUri) + if (inputStream == null) { + Timber.e("Failed to open input stream for checksum source %s", contentUri) + throw LocalFileNotFoundException() + } + + val sha1Hex = inputStream.use { input -> + TusChecksumHelper.sha1Hex(input) + } + persistTusChecksum(sha1Hex) + } + + private fun persistTusChecksum(sha1Hex: String) { + val checksum = TusChecksumHelper.storedSha1(sha1Hex).storageValue + transferRepository.updateTusChecksum( + id = uploadIdInStorageManager, + tusUploadChecksum = checksum, + ) + ocTransfer = ocTransfer.copy(tusUploadChecksum = checksum) + } + + private fun hasStoredSha1Checksum(): Boolean = + TusChecksumHelper.parseStoredChecksum(ocTransfer.tusUploadChecksum)?.uploadAlgorithm == + TusChecksumHelper.SHA1_WIRE_ALGORITHM + private fun clearTusState() { transferRepository.updateTusState( id = uploadIdInStorageManager, @@ -417,6 +513,15 @@ class UploadFileFromContentUriWorker( tusUploadExpires = null, tusUploadConcat = null, ) + ocTransfer = ocTransfer.copy( + tusUploadUrl = null, + tusUploadLength = null, + tusUploadMetadata = null, + tusUploadChecksum = null, + tusResumableVersion = null, + tusUploadExpires = null, + tusUploadConcat = null, + ) } private fun shouldRetry(throwable: Throwable?): Boolean { diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt index 7c32bccef1..7a97b003ff 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt @@ -53,6 +53,7 @@ import eu.opencloud.android.lib.common.operations.RemoteOperationResult.ResultCo import eu.opencloud.android.lib.resources.files.CheckPathExistenceRemoteOperation import eu.opencloud.android.lib.resources.files.CreateRemoteFolderOperation import eu.opencloud.android.lib.resources.files.UploadFileFromFileSystemOperation +import eu.opencloud.android.lib.resources.files.tus.TusChecksumHelper import eu.opencloud.android.presentation.authentication.AccountUtils import eu.opencloud.android.utils.NotificationUtils import eu.opencloud.android.utils.RemoteFileUtils.getAvailableRemotePath @@ -267,11 +268,16 @@ class UploadFileFromFileSystemWorker( ) if (shouldTryTus) { + if (hasPendingTusSession && !hasStoredSha1Checksum()) { + Timber.w("TUS session for %s has no original checksum. Clearing state and recreating.", uploadPath) + clearTusState() + } + ensureOriginalTusChecksum() Timber.d( "Attempting TUS upload (size=%d, threshold=%d, resume=%s)", fileSize, TusUploadHelper.DEFAULT_CHUNK_SIZE, - hasPendingTusSession + !ocTransfer.tusUploadUrl.isNullOrBlank() ) val tusSucceeded = try { val returnedEtag = tusUploadHelper.upload( @@ -376,8 +382,34 @@ class UploadFileFromFileSystemWorker( tusUploadExpires = null, tusUploadConcat = null, ) + ocTransfer = ocTransfer.copy( + tusUploadUrl = null, + tusUploadLength = null, + tusUploadMetadata = null, + tusUploadChecksum = null, + tusResumableVersion = null, + tusUploadExpires = null, + tusUploadConcat = null, + ) } + private fun ensureOriginalTusChecksum() { + if (hasStoredSha1Checksum()) return + + val checksum = TusChecksumHelper.storedSha1( + TusChecksumHelper.sha1Hex(File(fileSystemPath)) + ).storageValue + transferRepository.updateTusChecksum( + id = uploadIdInStorageManager, + tusUploadChecksum = checksum, + ) + ocTransfer = ocTransfer.copy(tusUploadChecksum = checksum) + } + + private fun hasStoredSha1Checksum(): Boolean = + TusChecksumHelper.parseStoredChecksum(ocTransfer.tusUploadChecksum)?.uploadAlgorithm == + TusChecksumHelper.SHA1_WIRE_ALGORITHM + private fun shouldRetry(throwable: Throwable?): Boolean { if (throwable == null) return false if (throwable is LocalFileNotFoundException) return false diff --git a/opencloudApp/src/test/java/eu/opencloud/android/workers/ContentUriUploadCacheValidatorTest.kt b/opencloudApp/src/test/java/eu/opencloud/android/workers/ContentUriUploadCacheValidatorTest.kt new file mode 100644 index 0000000000..0b2d88a9a1 --- /dev/null +++ b/opencloudApp/src/test/java/eu/opencloud/android/workers/ContentUriUploadCacheValidatorTest.kt @@ -0,0 +1,88 @@ +/** + * openCloud Android client application + * + * Copyright (C) 2026 OpenCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package eu.opencloud.android.workers + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class ContentUriUploadCacheValidatorTest { + + @Test + fun `exact large cache size is valid`() { + val fileSize = 5_832_800_958L + + val isValid = ContentUriUploadCacheValidator.isValidCacheSize( + actualSize = fileSize, + expectedSize = fileSize, + ) + + assertTrue(isValid) + } + + @Test + fun `partial non-zero cache size is invalid`() { + val isValid = ContentUriUploadCacheValidator.isValidCacheSize( + actualSize = 1_180_123_136L, + expectedSize = 5_832_800_958L, + ) + + assertFalse(isValid) + } + + @Test + fun `larger than expected cache size is invalid`() { + val isValid = ContentUriUploadCacheValidator.isValidCacheSize( + actualSize = 5_832_800_959L, + expectedSize = 5_832_800_958L, + ) + + assertFalse(isValid) + } + + @Test + fun `zero byte cache size is invalid`() { + val isValid = ContentUriUploadCacheValidator.isValidCacheSize( + actualSize = 0L, + expectedSize = 5_832_800_958L, + ) + + assertFalse(isValid) + } + + @Test + fun `unknown expected size keeps existing non-zero behavior`() { + val isValid = ContentUriUploadCacheValidator.isValidCacheSize( + actualSize = 42L, + expectedSize = -1L, + ) + + assertTrue(isValid) + } + + @Test + fun `non-positive expected size still rejects zero byte cache`() { + val isValid = ContentUriUploadCacheValidator.isValidCacheSize( + actualSize = 0L, + expectedSize = -1L, + ) + + assertFalse(isValid) + } +} diff --git a/opencloudApp/src/test/java/eu/opencloud/android/workers/TusUploadHelperTest.kt b/opencloudApp/src/test/java/eu/opencloud/android/workers/TusUploadHelperTest.kt index 4c9693c6aa..fe21c79e23 100644 --- a/opencloudApp/src/test/java/eu/opencloud/android/workers/TusUploadHelperTest.kt +++ b/opencloudApp/src/test/java/eu/opencloud/android/workers/TusUploadHelperTest.kt @@ -23,6 +23,7 @@ import androidx.test.core.app.ApplicationProvider import eu.opencloud.android.domain.capabilities.model.OCCapability import eu.opencloud.android.domain.transfers.TransferRepository import eu.opencloud.android.lib.common.OpenCloudClient +import eu.opencloud.android.lib.resources.files.tus.TusChecksumHelper import eu.opencloud.android.testutil.OC_TRANSFER import io.mockk.every import io.mockk.mockk @@ -33,6 +34,7 @@ import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull +import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -40,6 +42,7 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import java.io.File +import java.io.IOException @RunWith(RobolectricTestRunner::class) @Config(manifest = Config.NONE) @@ -62,21 +65,24 @@ class TusUploadHelperTest { } @Test - fun upload_createsSessionWithFirstChunkAndClearsTusState() { + fun upload_createsCheckedSessionWithoutFirstChunkAndClearsTusState() { val localFile = tempFileWithBytes(byteArrayOf(1, 2, 3, 4, 5)) + val sha1Hex = TusChecksumHelper.sha1Hex(localFile) + val storedChecksum = TusChecksumHelper.storedSha1(sha1Hex).storageValue val uploadUrl = "/uploads/new-session" server.enqueue( MockResponse() .setResponseCode(201) .addHeader("Location", uploadUrl) - .addHeader("Upload-Offset", "5") + .addHeader("Upload-Offset", "0") ) + server.enqueue(MockResponse().setResponseCode(204).addHeader("Upload-Offset", "5")) server.enqueue(MockResponse().setResponseCode(404)) val progress = mutableListOf() val resultEtag = TusUploadHelper(transferRepository).upload( client = newClient(), - transfer = OC_TRANSFER.copy(tusUploadUrl = null, tusUploadChecksum = "sha256:abc"), + transfer = OC_TRANSFER.copy(tusUploadUrl = null, tusUploadChecksum = storedChecksum), uploadId = UPLOAD_ID, localPath = localFile.absolutePath, remotePath = "/Photos/image.jpg", @@ -90,22 +96,35 @@ class TusUploadHelperTest { ) assertNull(resultEtag) - assertEquals(listOf(5L), progress) + assertEquals(listOf(0L, 5L), progress) val createRequest = server.takeRequest() assertEquals("POST", createRequest.method) assertEquals("/dav/spaces/personal/Photos", createRequest.path) - assertEquals("0", createRequest.getHeader("Upload-Offset")) + assertNull(createRequest.getHeader("Upload-Offset")) assertEquals("5", createRequest.getHeader("Upload-Length")) assertTrue(createRequest.getHeader("Upload-Metadata")!!.contains("checksum")) + val patchRequest = server.takeRequest() + assertEquals("PATCH", patchRequest.method) + assertEquals("0", patchRequest.getHeader("Upload-Offset")) + assertEquals( + TusChecksumHelper.uploadChecksumHeader( + file = localFile, + offset = 0, + length = 5, + algorithm = TusChecksumHelper.SHA1_WIRE_ALGORITHM, + ), + patchRequest.getHeader("Upload-Checksum") + ) + verify { transferRepository.updateTusState( id = UPLOAD_ID, tusUploadUrl = server.url(uploadUrl).toString(), tusUploadLength = 5, - tusUploadMetadata = "filename=image.jpg;mimetype=image/jpeg;mtime=1700000000;checksum=sha256 abc", - tusUploadChecksum = "sha256:abc", + tusUploadMetadata = "filename=image.jpg;mimetype=image/jpeg;mtime=1700000000;checksum=SHA1 $sha1Hex", + tusUploadChecksum = storedChecksum, tusResumableVersion = "1.0.0", tusUploadExpires = null, tusUploadConcat = null, @@ -161,6 +180,7 @@ class TusUploadHelperTest { assertEquals("PATCH", patchRequest.method) assertEquals("/uploads/existing-session", patchRequest.path) assertEquals("2", patchRequest.getHeader("Upload-Offset")) + assertNull(patchRequest.getHeader("Upload-Checksum")) verify(exactly = 1) { transferRepository.updateTusState( @@ -176,6 +196,64 @@ class TusUploadHelperTest { } } + @Test + fun upload_checksumMismatchClearsTusStateAndThrows() { + val localFile = tempFileWithBytes(byteArrayOf(1, 2, 3, 4, 5)) + val sha1Hex = TusChecksumHelper.sha1Hex(localFile) + val storedChecksum = TusChecksumHelper.storedSha1(sha1Hex).storageValue + val uploadUrl = "/uploads/checksum-mismatch" + server.enqueue( + MockResponse() + .setResponseCode(201) + .addHeader("Location", uploadUrl) + .addHeader("Upload-Offset", "0") + ) + server.enqueue(MockResponse().setResponseCode(460)) + + val thrown = assertThrows(IOException::class.java) { + TusUploadHelper(transferRepository).upload( + client = newClient(), + transfer = OC_TRANSFER.copy(tusUploadUrl = null, tusUploadChecksum = storedChecksum), + uploadId = UPLOAD_ID, + localPath = localFile.absolutePath, + remotePath = "/Photos/image.jpg", + fileSize = localFile.length(), + mimeType = "image/jpeg", + lastModified = "1700000000", + tusSupport = tusSupport(), + progressListener = null, + progressCallback = null, + spaceWebDavUrl = server.url("/dav/spaces/personal").toString(), + ) + } + + assertTrue(thrown.message!!.contains("checksum")) + server.takeRequest() + val patchRequest = server.takeRequest() + assertEquals("PATCH", patchRequest.method) + assertEquals( + TusChecksumHelper.uploadChecksumHeader( + file = localFile, + offset = 0, + length = 5, + algorithm = TusChecksumHelper.SHA1_WIRE_ALGORITHM, + ), + patchRequest.getHeader("Upload-Checksum") + ) + verify { + transferRepository.updateTusState( + id = UPLOAD_ID, + tusUploadUrl = null, + tusUploadLength = null, + tusUploadMetadata = null, + tusUploadChecksum = null, + tusResumableVersion = null, + tusUploadExpires = null, + tusUploadConcat = null, + ) + } + } + @Test fun shouldAttemptTusUpload_usesFallbackForSmallFilesWithoutPendingSession() { val shouldAttemptTusUpload = TusUploadHelper.shouldAttemptTusUpload( diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/HttpConstants.java b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/HttpConstants.java index 6bf34c99bc..8197c3be6f 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/HttpConstants.java +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/HttpConstants.java @@ -208,6 +208,8 @@ public class HttpConstants { // 424 Failed Dependency (WebDAV - RFC 2518) public static final int HTTP_FAILED_DEPENDENCY = 424; public static final int HTTP_TOO_EARLY = 425; + // 460 Checksum Mismatch (TUS checksum extension) + public static final int HTTP_CHECKSUM_MISMATCH = 460; /** * 5xx Client Error diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/PatchTusUploadChunkRemoteOperation.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/PatchTusUploadChunkRemoteOperation.kt index 5a173a0c4d..1964c60cd1 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/PatchTusUploadChunkRemoteOperation.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/PatchTusUploadChunkRemoteOperation.kt @@ -32,6 +32,7 @@ class PatchTusUploadChunkRemoteOperation( private val offset: Long, private val chunkSize: Long, private val httpMethodOverride: String? = null, + private val checksum: TusChecksumHelper.StoredChecksum? = null, ) : RemoteOperation() { private val cancellationRequested = AtomicBoolean(false) @@ -74,6 +75,17 @@ class PatchTusUploadChunkRemoteOperation( setRequestHeader(HttpConstants.TUS_RESUMABLE, HttpConstants.TUS_RESUMABLE_VERSION_1_0_0) setRequestHeader(HttpConstants.UPLOAD_OFFSET, offset.toString()) setRequestHeader(HttpConstants.CONTENT_TYPE_HEADER, HttpConstants.CONTENT_TYPE_OFFSET_OCTET_STREAM) + checksum?.let { + setRequestHeader( + HttpConstants.UPLOAD_CHECKSUM, + TusChecksumHelper.uploadChecksumHeader( + file = file, + offset = offset, + length = chunkSize, + algorithm = it.uploadAlgorithm, + ) + ) + } } activeMethod = method diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/TusChecksumHelper.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/TusChecksumHelper.kt new file mode 100644 index 0000000000..669b6a5eba --- /dev/null +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/TusChecksumHelper.kt @@ -0,0 +1,150 @@ +/** + * openCloud Android client application + * + * Copyright (C) 2026 OpenCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package eu.opencloud.android.lib.resources.files.tus + +import android.util.Base64 +import java.io.EOFException +import java.io.File +import java.io.FileInputStream +import java.io.InputStream +import java.io.OutputStream +import java.io.RandomAccessFile +import java.security.MessageDigest +import java.util.Locale +import kotlin.math.min + +object TusChecksumHelper { + private const val BUFFER_SIZE = 64 * 1024 + private const val SHA1_DIGEST_ALGORITHM = "SHA-1" + const val SHA1_WIRE_ALGORITHM = "sha1" + private const val SHA1_METADATA_ALGORITHM = "SHA1" + + data class StoredChecksum( + val algorithm: String, + val hex: String, + ) { + val storageValue: String + get() = "${algorithm.lowercase(Locale.ROOT)}:${hex.lowercase(Locale.ROOT)}" + + val metadataValue: String + get() = "${metadataAlgorithm()} ${hex.lowercase(Locale.ROOT)}" + + val uploadAlgorithm: String + get() = algorithm.lowercase(Locale.ROOT) + + private fun metadataAlgorithm(): String = + when (algorithm.lowercase(Locale.ROOT)) { + SHA1_WIRE_ALGORITHM -> SHA1_METADATA_ALGORITHM + else -> algorithm.uppercase(Locale.ROOT) + } + } + + data class CopyChecksumResult( + val bytesCopied: Long, + val sha1Hex: String, + ) + + fun storedSha1(hex: String): StoredChecksum = + StoredChecksum( + algorithm = SHA1_WIRE_ALGORITHM, + hex = hex.lowercase(Locale.ROOT), + ) + + fun parseStoredChecksum(value: String?): StoredChecksum? { + if (value.isNullOrBlank()) return null + + val separatorIndex = value.indexOf(':') + if (separatorIndex <= 0 || separatorIndex == value.lastIndex) return null + + val algorithm = value.substring(0, separatorIndex).trim().lowercase(Locale.ROOT) + val hex = value.substring(separatorIndex + 1).trim().lowercase(Locale.ROOT) + if (algorithm.isBlank() || hex.isBlank()) return null + + return StoredChecksum(algorithm = algorithm, hex = hex) + } + + fun sha1Hex(file: File): String = + FileInputStream(file).use { input -> + sha1Hex(input) + } + + fun sha1Hex(inputStream: InputStream): String { + val digest = MessageDigest.getInstance(SHA1_DIGEST_ALGORITHM) + val buffer = ByteArray(BUFFER_SIZE) + while (true) { + val read = inputStream.read(buffer) + if (read == -1) break + digest.update(buffer, 0, read) + } + return digest.digest().toHex() + } + + fun copyAndSha1Hex(inputStream: InputStream, outputStream: OutputStream): CopyChecksumResult { + val digest = MessageDigest.getInstance(SHA1_DIGEST_ALGORITHM) + val buffer = ByteArray(BUFFER_SIZE) + var copiedBytes = 0L + + while (true) { + val read = inputStream.read(buffer) + if (read == -1) break + outputStream.write(buffer, 0, read) + digest.update(buffer, 0, read) + copiedBytes += read.toLong() + } + + return CopyChecksumResult( + bytesCopied = copiedBytes, + sha1Hex = digest.digest().toHex(), + ) + } + + fun uploadChecksumHeader(file: File, offset: Long, length: Long, algorithm: String): String { + if (algorithm.lowercase(Locale.ROOT) != SHA1_WIRE_ALGORITHM) { + throw IllegalArgumentException("Unsupported TUS checksum algorithm: $algorithm") + } + val base64Digest = sha1Base64ForFileRange(file, offset, length) + return "$SHA1_WIRE_ALGORITHM $base64Digest" + } + + fun sha1Base64ForFileRange(file: File, offset: Long, length: Long): String { + require(offset >= 0) { "Offset must be non-negative" } + require(length >= 0) { "Length must be non-negative" } + + val digest = MessageDigest.getInstance(SHA1_DIGEST_ALGORITHM) + val buffer = ByteArray(BUFFER_SIZE) + var remaining = length + + RandomAccessFile(file, "r").use { raf -> + raf.seek(offset) + while (remaining > 0) { + val read = raf.read(buffer, 0, min(buffer.size.toLong(), remaining).toInt()) + if (read == -1) { + throw EOFException("Unable to read $length bytes from ${file.absolutePath} at offset $offset") + } + digest.update(buffer, 0, read) + remaining -= read.toLong() + } + } + + return Base64.encodeToString(digest.digest(), Base64.NO_WRAP) + } + + private fun ByteArray.toHex(): String = + joinToString(separator = "") { "%02x".format(it.toInt() and 0xff) } +} diff --git a/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusChecksumHelperTest.kt b/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusChecksumHelperTest.kt new file mode 100644 index 0000000000..4ba47dd06a --- /dev/null +++ b/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusChecksumHelperTest.kt @@ -0,0 +1,93 @@ +/** + * openCloud Android client application + * + * Copyright (C) 2026 OpenCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package eu.opencloud.android.lib.resources.files.tus + +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.security.MessageDigest +import java.util.Base64 + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class TusChecksumHelperTest { + + @Test + fun sha1Hex_returnsKnownDigest() { + val digest = TusChecksumHelper.sha1Hex(ByteArrayInputStream("hello".toByteArray())) + + assertEquals("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d", digest) + } + + @Test + fun copyAndSha1Hex_writesBytesAndReturnsDigest() { + val bytes = "original-upload-source".toByteArray() + val output = ByteArrayOutputStream() + + val result = TusChecksumHelper.copyAndSha1Hex(ByteArrayInputStream(bytes), output) + + assertEquals(bytes.size.toLong(), result.bytesCopied) + assertEquals(expectedSha1Hex(bytes), result.sha1Hex) + assertArrayEquals(bytes, output.toByteArray()) + } + + @Test + fun sha1Base64ForFileRange_returnsDigestForRequestedRange() { + val bytes = byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) + val file = tempFile(bytes) + val range = bytes.copyOfRange(3, 8) + + val digest = TusChecksumHelper.sha1Base64ForFileRange(file, offset = 3, length = 5) + + assertEquals(expectedSha1Base64(range), digest) + } + + @Test + fun copyAndSha1Hex_countsLargeCopiesPastBufferSize() { + val bytes = ByteArray(140_000) { index -> (index % 251).toByte() } + val output = ByteArrayOutputStream() + + val result = TusChecksumHelper.copyAndSha1Hex(ByteArrayInputStream(bytes), output) + + assertEquals(bytes.size.toLong(), result.bytesCopied) + assertEquals(expectedSha1Hex(bytes), result.sha1Hex) + assertArrayEquals(bytes, output.toByteArray()) + } + + private fun tempFile(bytes: ByteArray): File = + File.createTempFile("tus-checksum", ".bin").apply { + writeBytes(bytes) + } + + private fun expectedSha1Hex(bytes: ByteArray): String = + MessageDigest.getInstance("SHA-1") + .digest(bytes) + .joinToString(separator = "") { "%02x".format(it.toInt() and 0xff) } + + private fun expectedSha1Base64(bytes: ByteArray): String = + Base64.getEncoder().encodeToString( + MessageDigest.getInstance("SHA-1").digest(bytes) + ) +} diff --git a/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusIntegrationTest.kt b/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusIntegrationTest.kt index 944c7155ae..5b51efd8b1 100644 --- a/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusIntegrationTest.kt +++ b/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusIntegrationTest.kt @@ -61,6 +61,14 @@ class TusIntegrationTest { return client } + private fun decodeTusMetadata(header: String): Map = + header.split(",") + .filter { it.isNotBlank() } + .associate { entry -> + val parts = entry.split(" ", limit = 2) + parts[0] to String(Base64.getDecoder().decode(parts[1])) + } + @Test fun create_patch_head_delete_success() { val client = newClient() @@ -181,6 +189,90 @@ class TusIntegrationTest { assertEquals("1.0.0", delReq.getHeader("Tus-Resumable")) } + @Test + fun create_encodesChecksumMetadataWithoutCreationWithUpload() { + val client = newClient() + val collectionPath = "/remote.php/dav/uploads/$userId" + val locationPath = "$collectionPath/UPLD-checksum" + val localFile = File.createTempFile("tus", ".bin").apply { + writeBytes(byteArrayOf(1, 2, 3, 4, 5)) + } + val sha1Hex = TusChecksumHelper.sha1Hex(localFile) + server.enqueue( + MockResponse() + .setResponseCode(201) + .addHeader("Tus-Resumable", "1.0.0") + .addHeader("Location", locationPath) + .addHeader("Upload-Offset", "0") + ) + + val create = CreateTusUploadRemoteOperation( + file = localFile, + remotePath = "/test.bin", + mimetype = "application/octet-stream", + metadata = mapOf( + "filename" to "test.bin", + "checksum" to "SHA1 $sha1Hex", + ), + useCreationWithUpload = false, + firstChunkSize = null, + tusUrl = null, + collectionUrlOverride = server.url(collectionPath).toString(), + base64Encoder = object : Base64Encoder { + override fun encode(bytes: ByteArray): String = + Base64.getEncoder().encodeToString(bytes) + } + ) + + val createResult = create.execute(client) + assertTrue("Create operation failed", createResult.isSuccess) + + val postReq = server.takeRequest() + assertEquals("POST", postReq.method) + assertEquals("5", postReq.getHeader("Upload-Length")) + assertNull(postReq.getHeader("Upload-Offset")) + val metadata = decodeTusMetadata(postReq.getHeader("Upload-Metadata")!!) + assertEquals("test.bin", metadata["filename"]) + assertEquals("SHA1 $sha1Hex", metadata["checksum"]) + } + + @Test + fun patch_sendsUploadChecksumHeader() { + val client = newClient() + val locationPath = "/remote.php/dav/uploads/$userId/UPLD-checksum-patch" + val localFile = File.createTempFile("tus", ".bin").apply { + writeBytes(byteArrayOf(1, 2, 3, 4, 5)) + } + val checksum = TusChecksumHelper.storedSha1(TusChecksumHelper.sha1Hex(localFile)) + server.enqueue( + MockResponse() + .setResponseCode(204) + .addHeader("Upload-Offset", "5") + ) + + val patch = PatchTusUploadChunkRemoteOperation( + localPath = localFile.absolutePath, + uploadUrl = server.url(locationPath).toString(), + offset = 0, + chunkSize = 5, + checksum = checksum, + ) + val patchResult = patch.execute(client) + + assertTrue(patchResult.isSuccess) + val patchReq = server.takeRequest() + assertEquals("PATCH", patchReq.method) + assertEquals( + TusChecksumHelper.uploadChecksumHeader( + file = localFile, + offset = 0, + length = 5, + algorithm = TusChecksumHelper.SHA1_WIRE_ALGORITHM, + ), + patchReq.getHeader("Upload-Checksum") + ) + } + @Test fun creation_with_upload_returns_offset() { val client = newClient() diff --git a/opencloudData/src/main/java/eu/opencloud/android/data/transfers/datasources/LocalTransferDataSource.kt b/opencloudData/src/main/java/eu/opencloud/android/data/transfers/datasources/LocalTransferDataSource.kt index 764cc39deb..6a67b2c0b3 100644 --- a/opencloudData/src/main/java/eu/opencloud/android/data/transfers/datasources/LocalTransferDataSource.kt +++ b/opencloudData/src/main/java/eu/opencloud/android/data/transfers/datasources/LocalTransferDataSource.kt @@ -72,4 +72,6 @@ interface LocalTransferDataSource { ) fun updateTusUrl(id: Long, tusUploadUrl: String?) + + fun updateTusChecksum(id: Long, tusUploadChecksum: String?) } diff --git a/opencloudData/src/main/java/eu/opencloud/android/data/transfers/datasources/implementation/OCLocalTransferDataSource.kt b/opencloudData/src/main/java/eu/opencloud/android/data/transfers/datasources/implementation/OCLocalTransferDataSource.kt index 7b3101589d..a07f769ac9 100644 --- a/opencloudData/src/main/java/eu/opencloud/android/data/transfers/datasources/implementation/OCLocalTransferDataSource.kt +++ b/opencloudData/src/main/java/eu/opencloud/android/data/transfers/datasources/implementation/OCLocalTransferDataSource.kt @@ -171,6 +171,10 @@ class OCLocalTransferDataSource( transferDao.updateTusUrl(id = id, tusUploadUrl = tusUploadUrl) } + override fun updateTusChecksum(id: Long, tusUploadChecksum: String?) { + transferDao.updateTusChecksum(id = id, tusUploadChecksum = tusUploadChecksum) + } + companion object { diff --git a/opencloudData/src/main/java/eu/opencloud/android/data/transfers/db/TransferDao.kt b/opencloudData/src/main/java/eu/opencloud/android/data/transfers/db/TransferDao.kt index d23aedee0d..a86fda33d5 100644 --- a/opencloudData/src/main/java/eu/opencloud/android/data/transfers/db/TransferDao.kt +++ b/opencloudData/src/main/java/eu/opencloud/android/data/transfers/db/TransferDao.kt @@ -79,6 +79,9 @@ interface TransferDao { @Query(UPDATE_TUS_URL) fun updateTusUrl(id: Long, tusUploadUrl: String?) + @Query(UPDATE_TUS_CHECKSUM) + fun updateTusChecksum(id: Long, tusUploadChecksum: String?) + @Query(DELETE_TRANSFER_WITH_ID) fun deleteTransferWithId(id: Long) @@ -163,6 +166,12 @@ interface TransferDao { WHERE id = :id """ + private const val UPDATE_TUS_CHECKSUM = """ + UPDATE $TRANSFERS_TABLE_NAME + SET tusUploadChecksum = :tusUploadChecksum + WHERE id = :id + """ + private const val DELETE_TRANSFER_WITH_ID = """ DELETE FROM $TRANSFERS_TABLE_NAME diff --git a/opencloudData/src/main/java/eu/opencloud/android/data/transfers/repository/OCTransferRepository.kt b/opencloudData/src/main/java/eu/opencloud/android/data/transfers/repository/OCTransferRepository.kt index e27b95f459..447b4c4eb5 100644 --- a/opencloudData/src/main/java/eu/opencloud/android/data/transfers/repository/OCTransferRepository.kt +++ b/opencloudData/src/main/java/eu/opencloud/android/data/transfers/repository/OCTransferRepository.kt @@ -139,4 +139,7 @@ class OCTransferRepository( override fun updateTusUrl(id: Long, tusUploadUrl: String?) = localTransferDataSource.updateTusUrl(id = id, tusUploadUrl = tusUploadUrl) + + override fun updateTusChecksum(id: Long, tusUploadChecksum: String?) = + localTransferDataSource.updateTusChecksum(id = id, tusUploadChecksum = tusUploadChecksum) } diff --git a/opencloudData/src/test/java/eu/opencloud/android/data/transfers/repository/OCTransferRepositoryTest.kt b/opencloudData/src/test/java/eu/opencloud/android/data/transfers/repository/OCTransferRepositoryTest.kt index 8a87179d6c..197f115383 100644 --- a/opencloudData/src/test/java/eu/opencloud/android/data/transfers/repository/OCTransferRepositoryTest.kt +++ b/opencloudData/src/test/java/eu/opencloud/android/data/transfers/repository/OCTransferRepositoryTest.kt @@ -123,6 +123,17 @@ class OCTransferRepositoryTest { } } + @Test + fun `updateTusChecksum updates TUS checksum correctly`() { + val checksum = "sha1:aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d" + + ocTransferRepository.updateTusChecksum(OC_TRANSFER.id!!, checksum) + + verify(exactly = 1) { + localTransferDataSource.updateTusChecksum(OC_TRANSFER.id!!, checksum) + } + } + @Test fun `deleteTransferById removes a transfer correctly`() { ocTransferRepository.deleteTransferById(OC_TRANSFER.id!!) diff --git a/opencloudDomain/src/main/java/eu/opencloud/android/domain/transfers/TransferRepository.kt b/opencloudDomain/src/main/java/eu/opencloud/android/domain/transfers/TransferRepository.kt index 3fd9b8d72d..d5e4cac8e4 100644 --- a/opencloudDomain/src/main/java/eu/opencloud/android/domain/transfers/TransferRepository.kt +++ b/opencloudDomain/src/main/java/eu/opencloud/android/domain/transfers/TransferRepository.kt @@ -72,4 +72,6 @@ interface TransferRepository { ) fun updateTusUrl(id: Long, tusUploadUrl: String?) + + fun updateTusChecksum(id: Long, tusUploadChecksum: String?) } From ac4400265e80d26b0b47b8a4a682f3330bff4952 Mon Sep 17 00:00:00 2001 From: Markus Goetz Date: Wed, 10 Jun 2026 14:04:44 +0200 Subject: [PATCH 02/13] Checksums: Also get them via PROPFIND For debugging and potential later usage. For #181 --- .../common/http/methods/webdav/DavUtils.kt | 2 + .../methods/webdav/properties/OCChecksums.kt | 69 +++++++++++++++++++ .../files/ReadRemoteFolderOperation.kt | 2 + .../android/lib/resources/files/RemoteFile.kt | 6 ++ 4 files changed, 79 insertions(+) create mode 100644 opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/methods/webdav/properties/OCChecksums.kt diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/methods/webdav/DavUtils.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/methods/webdav/DavUtils.kt index be14d23e90..9fa4f20d45 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/methods/webdav/DavUtils.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/methods/webdav/DavUtils.kt @@ -36,6 +36,7 @@ import at.bitfire.dav4jvm.property.OCPermissions import at.bitfire.dav4jvm.property.OCPrivatelink import at.bitfire.dav4jvm.property.OCSize import at.bitfire.dav4jvm.property.ResourceType +import eu.opencloud.android.lib.common.http.methods.webdav.properties.OCChecksums import eu.opencloud.android.lib.common.http.methods.webdav.properties.OCShareTypes object DavUtils { @@ -53,6 +54,7 @@ object DavUtils { OCSize.NAME, OCPrivatelink.NAME, OCShareTypes.NAME, + OCChecksums.NAME, ) val quotaPropSet: Array diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/methods/webdav/properties/OCChecksums.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/methods/webdav/properties/OCChecksums.kt new file mode 100644 index 0000000000..8be1d9dc76 --- /dev/null +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/methods/webdav/properties/OCChecksums.kt @@ -0,0 +1,69 @@ +/* openCloud Android Library is available under MIT license + * Copyright (C) 2026 OpenCloud GmbH. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ +package eu.opencloud.android.lib.common.http.methods.webdav.properties + +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.PropertyFactory +import at.bitfire.dav4jvm.XmlUtils +import org.xmlpull.v1.XmlPullParser + +/** + * Parses `` from a PROPFIND response. + * + * Per the OpenCloud WebDAV docs, the body is a single `` child whose text is a + * whitespace-separated list of `ALGORITHM:value` pairs (documented as a historical quirk — + * "this value is not an array, but a string separated by whitespaces"): + * + * + * SHA1:1c68ea... MD5:2205e4... ADLER32:058801ab + * + * + * We split on whitespace at parse time so callers get a clean `List` of individual + * `ALGORITHM:value` entries. Tolerant of (a) multiple `` children, in case the + * server quirk is ever fixed, and (b) extra whitespace inside the text node. + */ +class OCChecksums(val checksums: List) : Property { + + override fun toString() = "checksums =[" + checksums.joinToString(", ") + "]" + + class Factory : PropertyFactory { + override fun getName(): Property.Name = NAME + + override fun create(parser: XmlPullParser): OCChecksums { + val raw = mutableListOf() + XmlUtils.readTextPropertyList(parser, CHECKSUM_NAME, raw) + // Flatten: each text node may itself contain multiple whitespace-separated + // "ALGORITHM:value" entries because of the documented server-side quirk. + val flattened = raw.flatMap { it.split(WHITESPACE) }.filter { it.isNotEmpty() } + return OCChecksums(flattened) + } + } + + companion object { + @JvmField + val NAME = Property.Name(XmlUtils.NS_OWNCLOUD, "checksums") + private val CHECKSUM_NAME = Property.Name(XmlUtils.NS_OWNCLOUD, "checksum") + private val WHITESPACE = Regex("\\s+") + } +} diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/ReadRemoteFolderOperation.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/ReadRemoteFolderOperation.kt index 48c4ca054d..d264be3a0f 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/ReadRemoteFolderOperation.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/ReadRemoteFolderOperation.kt @@ -31,6 +31,7 @@ import eu.opencloud.android.lib.common.http.HttpConstants.HTTP_OK import eu.opencloud.android.lib.common.http.methods.webdav.DavConstants import eu.opencloud.android.lib.common.http.methods.webdav.DavUtils import eu.opencloud.android.lib.common.http.methods.webdav.PropfindMethod +import eu.opencloud.android.lib.common.http.methods.webdav.properties.OCChecksums import eu.opencloud.android.lib.common.http.methods.webdav.properties.OCShareTypes import eu.opencloud.android.lib.common.network.WebdavUtils import eu.opencloud.android.lib.common.operations.RemoteOperation @@ -60,6 +61,7 @@ class ReadRemoteFolderOperation( override fun run(client: OpenCloudClient): RemoteOperationResult> { try { PropertyRegistry.register(OCShareTypes.Factory()) + PropertyRegistry.register(OCChecksums.Factory()) val propfindMethod = PropfindMethod( getFinalWebDavUrl(), diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/RemoteFile.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/RemoteFile.kt index 4743ab7d9c..719e3f3a76 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/RemoteFile.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/RemoteFile.kt @@ -39,6 +39,7 @@ import at.bitfire.dav4jvm.property.OCPermissions import at.bitfire.dav4jvm.property.OCPrivatelink import at.bitfire.dav4jvm.property.OCSize import eu.opencloud.android.lib.common.http.HttpConstants +import eu.opencloud.android.lib.common.http.methods.webdav.properties.OCChecksums import eu.opencloud.android.lib.common.http.methods.webdav.properties.OCShareTypes import eu.opencloud.android.lib.common.utils.isOneOf import eu.opencloud.android.lib.resources.shares.ShareType @@ -73,6 +74,8 @@ data class RemoteFile( var owner: String, var sharedByLink: Boolean = false, var sharedWithSharee: Boolean = false, + /** Server-reported checksums as raw "ALGORITHM:value" strings (e.g. "SHA1:1c68ea…"). */ + var checksums: List = emptyList(), ) : Parcelable { // To do: Quotas not used. Use or remove them. @@ -134,6 +137,9 @@ data class RemoteFile( is OCPrivatelink -> { remoteFile.privateLink = property.link } + is OCChecksums -> { + remoteFile.checksums = property.checksums + } is OCShareTypes -> { val list = property.shareTypes for (i in list.indices) { From b0fbc4656d5471169d83b3181b728e17c89863d2 Mon Sep 17 00:00:00 2001 From: Markus Goetz Date: Wed, 10 Jun 2026 15:23:17 +0200 Subject: [PATCH 03/13] Fix detekt --- .../android/lib/resources/files/tus/TusChecksumHelper.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/TusChecksumHelper.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/TusChecksumHelper.kt index 669b6a5eba..6f0e88cfb2 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/TusChecksumHelper.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/TusChecksumHelper.kt @@ -49,10 +49,8 @@ object TusChecksumHelper { get() = algorithm.lowercase(Locale.ROOT) private fun metadataAlgorithm(): String = - when (algorithm.lowercase(Locale.ROOT)) { - SHA1_WIRE_ALGORITHM -> SHA1_METADATA_ALGORITHM - else -> algorithm.uppercase(Locale.ROOT) - } + if (algorithm.lowercase(Locale.ROOT) == SHA1_WIRE_ALGORITHM) SHA1_METADATA_ALGORITHM + else algorithm.uppercase(Locale.ROOT) } data class CopyChecksumResult( From cee8059cd4d959c73e0e4cf03300679ac1ddfdea Mon Sep 17 00:00:00 2001 From: Markus Goetz Date: Thu, 11 Jun 2026 11:21:46 +0200 Subject: [PATCH 04/13] TUS: Use creation with upload again after checksum feature --- .../android/workers/TusUploadHelper.kt | 38 +++++++------ .../android/workers/TusUploadHelperTest.kt | 19 ++++--- .../tus/CreateTusUploadRemoteOperation.kt | 21 +++++++- .../tus/PatchTusUploadChunkRemoteOperation.kt | 19 ++++--- .../resources/files/tus/TusIntegrationTest.kt | 54 ++++++++++++++++++- 5 files changed, 110 insertions(+), 41 deletions(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt index a0e77f5145..4abcf9b54e 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt @@ -55,7 +55,7 @@ class TusUploadHelper( ) : String? { // Reset cancelled state for new upload cancelled = false - val checksum = TusChecksumHelper.parseStoredChecksum(transfer.tusUploadChecksum) + val fileChecksum = TusChecksumHelper.parseStoredChecksum(transfer.tusUploadChecksum) ?.takeIf { it.uploadAlgorithm == TusChecksumHelper.SHA1_WIRE_ALGORITHM } Timber.d("TUS: starting upload for %s size=%d", remotePath, fileSize) @@ -69,7 +69,8 @@ class TusUploadHelper( mimeType = mimeType, lastModified = lastModified, spaceWebDavUrl = spaceWebDavUrl, - checksum = checksum, + fileChecksum = fileChecksum, + tusSupport = tusSupport, ) val offset = fetchCurrentOffset(client, resolvedTusUrl, createdOffset) @@ -86,7 +87,7 @@ class TusUploadHelper( progressCallback = progressCallback, initialOffset = offset, uploadId = uploadId, - checksum = checksum, + checksumAlgorithm = fileChecksum?.uploadAlgorithm, ) verifyUploadCompletion(finalOffset, fileSize, uploadId) @@ -104,7 +105,8 @@ class TusUploadHelper( mimeType: String, lastModified: String?, spaceWebDavUrl: String?, - checksum: TusChecksumHelper.StoredChecksum?, + fileChecksum: TusChecksumHelper.StoredChecksum?, + tusSupport: OCCapability.TusSupport?, ): Pair { var tusUrl = transfer.tusUploadUrl var createdOffset: Long? = null @@ -116,7 +118,7 @@ class TusUploadHelper( "mimetype" to mimeType, ) lastModified?.takeIf { it.isNotBlank() }?.let { metadata["mtime"] = it } - checksum?.let { metadata["checksum"] = it.metadataValue } + fileChecksum?.let { metadata["checksum"] = it.metadataValue } Timber.d( "TUS: creating upload resource filename=%s size=%d metadata=%s", @@ -130,23 +132,25 @@ class TusUploadHelper( spaceWebDavUrl = spaceWebDavUrl ) - // Checked uploads must send every byte via PATCH so each chunk can carry Upload-Checksum. - val useCreationWithUpload = checksum == null - val firstChunkSize = if (useCreationWithUpload) { - minOf(CreateTusUploadRemoteOperation.DEFAULT_FIRST_CHUNK, fileSize) - } else { - null - } + // Use creation-with-upload like the browser does for OpenCloud compatibility. + // The data part of a creation-with-upload POST follows the same rules as a PATCH + // (TUS spec), so it carries Upload-Checksum for the first chunk just like the + // PATCH requests do for the remaining ones — and like a PATCH it must respect + // the server's max_chunk_size (DEFAULT_FIRST_CHUNK is 10 MiB, but e.g. OpenCloud + // advertises 10_000_000, slightly smaller). + val serverMaxChunk = tusSupport?.maxChunkSize?.takeIf { it > 0 }?.toLong() ?: Long.MAX_VALUE + val firstChunkSize = minOf(CreateTusUploadRemoteOperation.DEFAULT_FIRST_CHUNK, fileSize, serverMaxChunk) val creationResult = executeRemoteOperation { CreateTusUploadRemoteOperation( file = File(localPath), remotePath = remotePath, mimetype = mimeType, metadata = metadata, - useCreationWithUpload = useCreationWithUpload, + useCreationWithUpload = true, firstChunkSize = firstChunkSize, tusUrl = "", collectionUrlOverride = collectionUrl, + checksumAlgorithm = fileChecksum?.uploadAlgorithm, ).execute(client) } @@ -163,7 +167,7 @@ class TusUploadHelper( tusUploadUrl = tusUrl, tusUploadLength = fileSize, tusUploadMetadata = metadataString, - tusUploadChecksum = checksum?.storageValue, + tusUploadChecksum = fileChecksum?.storageValue, tusResumableVersion = "1.0.0", tusUploadExpires = null, tusUploadConcat = null, @@ -243,7 +247,7 @@ class TusUploadHelper( progressCallback: ((Long, Long) -> Unit)?, initialOffset: Long, uploadId: Long, - checksum: TusChecksumHelper.StoredChecksum?, + checksumAlgorithm: String?, ): Pair { var offset = initialOffset var lastEtag: String? = null @@ -267,7 +271,7 @@ class TusUploadHelper( offset = offset, chunkSize = chunkSize, httpMethodOverride = httpOverride, - checksum = checksum, + checksumAlgorithm = checksumAlgorithm, ).apply { progressListener?.let { addDataTransferProgressListener(it) } } @@ -276,7 +280,7 @@ class TusUploadHelper( val patchResult = patchOperation.execute(client) lastEtag = patchOperation.etag.takeIf { it.isNotBlank() } activePatchOperation = null - if (checksum != null && isChecksumFailure(patchResult.httpCode)) { + if (checksumAlgorithm != null && isChecksumFailure(patchResult.httpCode)) { clearTusState(uploadId) throw java.io.IOException( "TUS: checksum upload rejected with HTTP ${patchResult.httpCode} at offset $offset" diff --git a/opencloudApp/src/test/java/eu/opencloud/android/workers/TusUploadHelperTest.kt b/opencloudApp/src/test/java/eu/opencloud/android/workers/TusUploadHelperTest.kt index fe21c79e23..efe91739ff 100644 --- a/opencloudApp/src/test/java/eu/opencloud/android/workers/TusUploadHelperTest.kt +++ b/opencloudApp/src/test/java/eu/opencloud/android/workers/TusUploadHelperTest.kt @@ -31,6 +31,7 @@ import io.mockk.verify import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.After +import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull @@ -65,18 +66,19 @@ class TusUploadHelperTest { } @Test - fun upload_createsCheckedSessionWithoutFirstChunkAndClearsTusState() { + fun upload_createsCheckedSessionWithFirstChunkChecksumAndClearsTusState() { val localFile = tempFileWithBytes(byteArrayOf(1, 2, 3, 4, 5)) val sha1Hex = TusChecksumHelper.sha1Hex(localFile) val storedChecksum = TusChecksumHelper.storedSha1(sha1Hex).storageValue val uploadUrl = "/uploads/new-session" + // creation-with-upload: the whole 5-byte file fits in the creation POST, + // so the server acknowledges Upload-Offset 5 and no PATCH follows. server.enqueue( MockResponse() .setResponseCode(201) .addHeader("Location", uploadUrl) - .addHeader("Upload-Offset", "0") + .addHeader("Upload-Offset", "5") ) - server.enqueue(MockResponse().setResponseCode(204).addHeader("Upload-Offset", "5")) server.enqueue(MockResponse().setResponseCode(404)) val progress = mutableListOf() @@ -96,18 +98,14 @@ class TusUploadHelperTest { ) assertNull(resultEtag) - assertEquals(listOf(0L, 5L), progress) + assertEquals(listOf(5L), progress) val createRequest = server.takeRequest() assertEquals("POST", createRequest.method) assertEquals("/dav/spaces/personal/Photos", createRequest.path) - assertNull(createRequest.getHeader("Upload-Offset")) + assertEquals("0", createRequest.getHeader("Upload-Offset")) assertEquals("5", createRequest.getHeader("Upload-Length")) assertTrue(createRequest.getHeader("Upload-Metadata")!!.contains("checksum")) - - val patchRequest = server.takeRequest() - assertEquals("PATCH", patchRequest.method) - assertEquals("0", patchRequest.getHeader("Upload-Offset")) assertEquals( TusChecksumHelper.uploadChecksumHeader( file = localFile, @@ -115,8 +113,9 @@ class TusUploadHelperTest { length = 5, algorithm = TusChecksumHelper.SHA1_WIRE_ALGORITHM, ), - patchRequest.getHeader("Upload-Checksum") + createRequest.getHeader("Upload-Checksum") ) + assertArrayEquals(byteArrayOf(1, 2, 3, 4, 5), createRequest.body.readByteArray()) verify { transferRepository.updateTusState( diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/CreateTusUploadRemoteOperation.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/CreateTusUploadRemoteOperation.kt index b482fc5f21..ad2ced1eed 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/CreateTusUploadRemoteOperation.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/CreateTusUploadRemoteOperation.kt @@ -29,7 +29,9 @@ class CreateTusUploadRemoteOperation( private val firstChunkSize: Long?, private val tusUrl: String?, private val collectionUrlOverride: String? = null, - private val base64Encoder: Base64Encoder = DefaultBase64Encoder() + private val base64Encoder: Base64Encoder = DefaultBase64Encoder(), + /** When set (e.g. "sha1"), an Upload-Checksum header is computed over the first chunk. */ + private val checksumAlgorithm: String? = null, ) : RemoteOperation() { data class CreationResult( @@ -103,7 +105,7 @@ class CreateTusUploadRemoteOperation( // Set Upload-Offset for creation-with-upload if (useCreationWithUpload && (firstChunkSize ?: 0L) > 0L) { - postMethod.setRequestHeader(HttpConstants.UPLOAD_OFFSET, "0") + setCreationWithUploadHeaders(postMethod, firstChunkSize!!) } val status = client.executeHttpMethod(postMethod) @@ -164,6 +166,21 @@ class CreateTusUploadRemoteOperation( result } + private fun setCreationWithUploadHeaders(postMethod: PostMethod, firstChunkSize: Long) { + postMethod.setRequestHeader(HttpConstants.UPLOAD_OFFSET, "0") + // The data part of a creation-with-upload POST follows the same rules as a + // PATCH (TUS spec), so it carries Upload-Checksum for the first chunk. + checksumAlgorithm?.let { algorithm -> + val chunkChecksumHeader = TusChecksumHelper.uploadChecksumHeader( + file = file, + offset = 0, + length = firstChunkSize, + algorithm = algorithm, + ) + postMethod.setRequestHeader(HttpConstants.UPLOAD_CHECKSUM, chunkChecksumHeader) + } + } + private fun isSuccess(status: Int) = status.isOneOf(HttpConstants.HTTP_CREATED, HttpConstants.HTTP_OK) diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/PatchTusUploadChunkRemoteOperation.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/PatchTusUploadChunkRemoteOperation.kt index 1964c60cd1..2d08087d51 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/PatchTusUploadChunkRemoteOperation.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/PatchTusUploadChunkRemoteOperation.kt @@ -32,7 +32,8 @@ class PatchTusUploadChunkRemoteOperation( private val offset: Long, private val chunkSize: Long, private val httpMethodOverride: String? = null, - private val checksum: TusChecksumHelper.StoredChecksum? = null, + /** When set (e.g. "sha1"), an Upload-Checksum header is computed over this chunk's bytes. */ + private val checksumAlgorithm: String? = null, ) : RemoteOperation() { private val cancellationRequested = AtomicBoolean(false) @@ -75,16 +76,14 @@ class PatchTusUploadChunkRemoteOperation( setRequestHeader(HttpConstants.TUS_RESUMABLE, HttpConstants.TUS_RESUMABLE_VERSION_1_0_0) setRequestHeader(HttpConstants.UPLOAD_OFFSET, offset.toString()) setRequestHeader(HttpConstants.CONTENT_TYPE_HEADER, HttpConstants.CONTENT_TYPE_OFFSET_OCTET_STREAM) - checksum?.let { - setRequestHeader( - HttpConstants.UPLOAD_CHECKSUM, - TusChecksumHelper.uploadChecksumHeader( - file = file, - offset = offset, - length = chunkSize, - algorithm = it.uploadAlgorithm, - ) + checksumAlgorithm?.let { algorithm -> + val chunkChecksumHeader = TusChecksumHelper.uploadChecksumHeader( + file = file, + offset = offset, + length = chunkSize, + algorithm = algorithm, ) + setRequestHeader(HttpConstants.UPLOAD_CHECKSUM, chunkChecksumHeader) } } diff --git a/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusIntegrationTest.kt b/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusIntegrationTest.kt index 5b51efd8b1..9884a5b0fe 100644 --- a/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusIntegrationTest.kt +++ b/opencloudComLibrary/src/test/java/eu/opencloud/android/lib/resources/files/tus/TusIntegrationTest.kt @@ -243,7 +243,6 @@ class TusIntegrationTest { val localFile = File.createTempFile("tus", ".bin").apply { writeBytes(byteArrayOf(1, 2, 3, 4, 5)) } - val checksum = TusChecksumHelper.storedSha1(TusChecksumHelper.sha1Hex(localFile)) server.enqueue( MockResponse() .setResponseCode(204) @@ -255,7 +254,7 @@ class TusIntegrationTest { uploadUrl = server.url(locationPath).toString(), offset = 0, chunkSize = 5, - checksum = checksum, + checksumAlgorithm = TusChecksumHelper.SHA1_WIRE_ALGORITHM, ) val patchResult = patch.execute(client) @@ -339,6 +338,57 @@ class TusIntegrationTest { assertEquals(firstChunkSize.toString(), postReq.getHeader("Content-Length")) } + @Test + fun creation_with_upload_sendsUploadChecksumForFirstChunk() { + val client = newClient() + val collectionPath = "/remote.php/dav/uploads/$userId" + val locationPath = "$collectionPath/UPLD-WITH-DATA-CHECKSUM" + val localFile = File.createTempFile("tus", ".bin").apply { + writeBytes(ByteArray(100) { it.toByte() }) + } + val firstChunkSize = 50L + server.enqueue( + MockResponse() + .setResponseCode(201) + .addHeader("Tus-Resumable", "1.0.0") + .addHeader("Location", locationPath) + .addHeader("Upload-Offset", firstChunkSize.toString()) + ) + + val create = CreateTusUploadRemoteOperation( + file = localFile, + remotePath = "/test-with-data.bin", + mimetype = "application/octet-stream", + metadata = mapOf("filename" to "test-with-data.bin"), + useCreationWithUpload = true, + firstChunkSize = firstChunkSize, + tusUrl = null, + collectionUrlOverride = server.url(collectionPath).toString(), + base64Encoder = object : Base64Encoder { + override fun encode(bytes: ByteArray): String = + Base64.getEncoder().encodeToString(bytes) + }, + checksumAlgorithm = TusChecksumHelper.SHA1_WIRE_ALGORITHM, + ) + + val createResult = create.execute(client) + assertTrue("Create operation failed", createResult.isSuccess) + + val postReq = server.takeRequest() + assertEquals("POST", postReq.method) + assertEquals("0", postReq.getHeader("Upload-Offset")) + // The Upload-Checksum header must cover exactly the first chunk, not the whole file. + assertEquals( + TusChecksumHelper.uploadChecksumHeader( + file = localFile, + offset = 0, + length = firstChunkSize, + algorithm = TusChecksumHelper.SHA1_WIRE_ALGORITHM, + ), + postReq.getHeader("Upload-Checksum") + ) + } + @Test fun patch_wrong_offset_returns_conflict() { val client = newClient() From dbe26b44378fa7b96054b94dd4e409b2ca87e8e0 Mon Sep 17 00:00:00 2001 From: Markus Goetz Date: Thu, 11 Jun 2026 11:48:08 +0200 Subject: [PATCH 05/13] Performance: Remove peobably useless PROPFIND Was introduced by fa61c6d59. --- .../resources/files/DownloadRemoteFileOperation.kt | 14 ++------------ .../files/UploadFileFromFileSystemOperation.kt | 12 +----------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/DownloadRemoteFileOperation.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/DownloadRemoteFileOperation.kt index 005aa600a3..ecf1f9e532 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/DownloadRemoteFileOperation.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/DownloadRemoteFileOperation.kt @@ -26,9 +26,6 @@ package eu.opencloud.android.lib.resources.files import eu.opencloud.android.lib.common.OpenCloudClient import eu.opencloud.android.lib.common.http.HttpConstants import eu.opencloud.android.lib.common.http.methods.nonwebdav.GetMethod -import eu.opencloud.android.lib.common.http.methods.webdav.DavConstants -import eu.opencloud.android.lib.common.http.methods.webdav.DavUtils -import eu.opencloud.android.lib.common.http.methods.webdav.PropfindMethod import eu.opencloud.android.lib.common.network.OnDatatransferProgressListener import eu.opencloud.android.lib.common.network.WebdavUtils import eu.opencloud.android.lib.common.operations.OperationCancelledException @@ -68,18 +65,11 @@ class DownloadRemoteFileOperation( // download will be performed to a temporal file, then moved to the final location val tmpFile = File(tmpPath) - val propfindMethod = PropfindMethod( - URL(client.userFilesWebDavUri.toString()), - DavConstants.DEPTH_1, - DavUtils.allPropSet - ) - val status = client.executeHttpMethod(propfindMethod) - // perform the download return try { tmpFile.parentFile?.mkdirs() - downloadFile(client, tmpFile).also { - Timber.i("Download of $remotePath to $tmpPath - HTTP status code: $status") + downloadFile(client, tmpFile).also { result -> + Timber.i("Download of $remotePath to $tmpPath: ${result.logMessage}") } } catch (e: Exception) { RemoteOperationResult(e).also { result -> diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/UploadFileFromFileSystemOperation.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/UploadFileFromFileSystemOperation.kt index 65cb09f1fe..742e76bcc6 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/UploadFileFromFileSystemOperation.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/UploadFileFromFileSystemOperation.kt @@ -25,9 +25,6 @@ package eu.opencloud.android.lib.resources.files import eu.opencloud.android.lib.common.OpenCloudClient import eu.opencloud.android.lib.common.http.HttpConstants -import eu.opencloud.android.lib.common.http.methods.webdav.DavConstants -import eu.opencloud.android.lib.common.http.methods.webdav.DavUtils -import eu.opencloud.android.lib.common.http.methods.webdav.PropfindMethod import eu.opencloud.android.lib.common.http.methods.webdav.PutMethod import eu.opencloud.android.lib.common.network.FileRequestBody import eu.opencloud.android.lib.common.network.OnDatatransferProgressListener @@ -72,13 +69,6 @@ open class UploadFileFromFileSystemOperation( override fun run(client: OpenCloudClient): RemoteOperationResult { var result: RemoteOperationResult try { - val propfindMethod = PropfindMethod( - URL(client.userFilesWebDavUri.toString()), - DavConstants.DEPTH_1, - DavUtils.allPropSet - ) - val status = client.executeHttpMethod(propfindMethod) - if (cancellationRequested.get()) { // the operation was cancelled before getting it's turn to be executed in the queue of uploads result = RemoteOperationResult(OperationCancelledException()) @@ -86,7 +76,7 @@ open class UploadFileFromFileSystemOperation( } else { // perform the upload result = uploadFile(client) - Timber.i("Upload of $localPath to $remotePath - HTTP status code: $status") + Timber.i("Upload of $localPath to $remotePath: ${result.logMessage}") } } catch (e: Exception) { if (putMethod?.isAborted == true) { From 573812edaebfd8f316e4ba3fcaf0121f6ca14ef6 Mon Sep 17 00:00:00 2001 From: Markus Goetz Date: Thu, 11 Jun 2026 12:26:44 +0200 Subject: [PATCH 06/13] Uploads: Send OC-Checksum for non-TUS --- .../workers/UploadFileFromContentUriWorker.kt | 16 +++++++++++----- .../workers/UploadFileFromFileSystemWorker.kt | 15 ++++++++++----- .../android/lib/common/http/HttpConstants.java | 1 + .../files/DownloadRemoteFileOperation.kt | 3 ++- .../files/UploadFileFromFileSystemOperation.kt | 6 +++++- .../lib/resources/files/tus/TusChecksumHelper.kt | 4 ++++ 6 files changed, 33 insertions(+), 12 deletions(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt index f5a636b9af..b26924373b 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt @@ -377,12 +377,16 @@ class UploadFileFromContentUriWorker( tusUploadUrl = ocTransfer.tusUploadUrl, ) + if (hasPendingTusSession && !hasStoredSha1Checksum()) { + Timber.w("TUS session for %s has no original checksum. Clearing state and recreating.", uploadPath) + clearTusState() + } + // Always have the whole-file checksum: TUS sends it in Upload-Metadata, plain PUTs + // in the OC-Checksum header. Usually already persisted by copyFileToLocalStorage; + // this only reads the source again for cache-reuse runs of pre-checksum DB rows. + ensureOriginalTusChecksum() + if (shouldTryTus) { - if (hasPendingTusSession && !hasStoredSha1Checksum()) { - Timber.w("TUS session for %s has no original checksum. Clearing state and recreating.", uploadPath) - clearTusState() - } - ensureOriginalTusChecksum() Timber.d( "Attempting TUS upload (size=%d, threshold=%d, resume=%s)", fileSize, @@ -434,6 +438,7 @@ class UploadFileFromContentUriWorker( } private fun uploadPlainFile(client: OpenCloudClient) { + val fileChecksum = TusChecksumHelper.parseStoredChecksum(ocTransfer.tusUploadChecksum) uploadFileOperation = UploadFileFromFileSystemOperation( localPath = cachePath, remotePath = uploadPath, @@ -441,6 +446,7 @@ class UploadFileFromContentUriWorker( lastModifiedTimestamp = lastModified, requiredEtag = null, spaceWebDavUrl = spaceWebDavUrl, + ocChecksum = fileChecksum?.ocChecksumHeaderValue, ).apply { addDataTransferProgressListener(this@UploadFileFromContentUriWorker) } diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt index 7a97b003ff..3314368f8c 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt @@ -267,12 +267,15 @@ class UploadFileFromFileSystemWorker( tusUploadUrl = ocTransfer.tusUploadUrl, ) + if (hasPendingTusSession && !hasStoredSha1Checksum()) { + Timber.w("TUS session for %s has no original checksum. Clearing state and recreating.", uploadPath) + clearTusState() + } + // Always compute the whole-file checksum: TUS sends it in Upload-Metadata, + // plain PUTs send it in the OC-Checksum header. + ensureOriginalTusChecksum() + if (shouldTryTus) { - if (hasPendingTusSession && !hasStoredSha1Checksum()) { - Timber.w("TUS session for %s has no original checksum. Clearing state and recreating.", uploadPath) - clearTusState() - } - ensureOriginalTusChecksum() Timber.d( "Attempting TUS upload (size=%d, threshold=%d, resume=%s)", fileSize, @@ -326,6 +329,7 @@ class UploadFileFromFileSystemWorker( } private fun uploadPlainFile(client: OpenCloudClient) { + val fileChecksum = TusChecksumHelper.parseStoredChecksum(ocTransfer.tusUploadChecksum) uploadFileOperation = UploadFileFromFileSystemOperation( localPath = fileSystemPath, remotePath = uploadPath, @@ -333,6 +337,7 @@ class UploadFileFromFileSystemWorker( lastModifiedTimestamp = lastModified, requiredEtag = eTagInConflict, spaceWebDavUrl = spaceWebDavUrl, + ocChecksum = fileChecksum?.ocChecksumHeaderValue, ).apply { addDataTransferProgressListener(this@UploadFileFromFileSystemWorker) } diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/HttpConstants.java b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/HttpConstants.java index 8197c3be6f..e5f53bdcc9 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/HttpConstants.java +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/HttpConstants.java @@ -44,6 +44,7 @@ public class HttpConstants { public static final String CONTENT_LENGTH_HEADER = "Content-Length"; public static final String OC_TOTAL_LENGTH_HEADER = "OC-Total-Length"; public static final String OC_X_OC_MTIME_HEADER = "X-OC-Mtime"; + public static final String OC_CHECKSUM_HEADER = "OC-Checksum"; public static final String OC_X_REQUEST_ID = "X-Request-ID"; public static final String X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override"; public static final String LOCATION_HEADER = "Location"; diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/DownloadRemoteFileOperation.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/DownloadRemoteFileOperation.kt index ecf1f9e532..7b01d44d8b 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/DownloadRemoteFileOperation.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/DownloadRemoteFileOperation.kt @@ -69,7 +69,8 @@ class DownloadRemoteFileOperation( return try { tmpFile.parentFile?.mkdirs() downloadFile(client, tmpFile).also { result -> - Timber.i("Download of $remotePath to $tmpPath: ${result.logMessage}") + val outcome = if (result.isSuccess) "success, etag=$etag" else result.logMessage + Timber.i("Download of $remotePath to $tmpPath: $outcome") } } catch (e: Exception) { RemoteOperationResult(e).also { result -> diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/UploadFileFromFileSystemOperation.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/UploadFileFromFileSystemOperation.kt index 742e76bcc6..169dd51fe7 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/UploadFileFromFileSystemOperation.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/UploadFileFromFileSystemOperation.kt @@ -57,6 +57,8 @@ open class UploadFileFromFileSystemOperation( val lastModifiedTimestamp: String, val requiredEtag: String?, val spaceWebDavUrl: String? = null, + /** Whole-file checksum as "ALGORITHM:hex" (e.g. "SHA1:30338d…"); server rejects with 400 on mismatch. */ + val ocChecksum: String? = null, ) : RemoteOperation() { protected val cancellationRequested = AtomicBoolean(false) @@ -76,7 +78,8 @@ open class UploadFileFromFileSystemOperation( } else { // perform the upload result = uploadFile(client) - Timber.i("Upload of $localPath to $remotePath: ${result.logMessage}") + val outcome = if (result.isSuccess) "success, etag=$etag" else result.logMessage + Timber.i("Upload of $localPath to $remotePath: $outcome") } } catch (e: Exception) { if (putMethod?.isAborted == true) { @@ -107,6 +110,7 @@ open class UploadFileFromFileSystemOperation( } addRequestHeader(HttpConstants.OC_TOTAL_LENGTH_HEADER, fileToUpload.length().toString()) addRequestHeader(HttpConstants.OC_X_OC_MTIME_HEADER, lastModifiedTimestamp) + ocChecksum?.let { addRequestHeader(HttpConstants.OC_CHECKSUM_HEADER, it) } } val status = client.executeHttpMethod(putMethod) diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/TusChecksumHelper.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/TusChecksumHelper.kt index 6f0e88cfb2..46b691b7fa 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/TusChecksumHelper.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/tus/TusChecksumHelper.kt @@ -48,6 +48,10 @@ object TusChecksumHelper { val uploadAlgorithm: String get() = algorithm.lowercase(Locale.ROOT) + /** Value for the OC-Checksum header on plain PUTs: "SHA1:" (colon-separated). */ + val ocChecksumHeaderValue: String + get() = "${metadataAlgorithm()}:${hex.lowercase(Locale.ROOT)}" + private fun metadataAlgorithm(): String = if (algorithm.lowercase(Locale.ROOT) == SHA1_WIRE_ALGORITHM) SHA1_METADATA_ALGORITHM else algorithm.uppercase(Locale.ROOT) From 8d0c15bfaa7517acb8c5078c610d71b3eb9714c1 Mon Sep 17 00:00:00 2001 From: Markus Goetz Date: Thu, 11 Jun 2026 14:57:11 +0200 Subject: [PATCH 07/13] Uploads: Fix mimetype --- .../android/workers/UploadFileFromContentUriWorker.kt | 3 ++- .../android/workers/UploadFileFromFileSystemWorker.kt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt index b26924373b..4348a71eb1 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt @@ -62,6 +62,7 @@ import eu.opencloud.android.lib.resources.files.CreateRemoteFolderOperation import eu.opencloud.android.lib.resources.files.UploadFileFromFileSystemOperation import eu.opencloud.android.lib.resources.files.tus.TusChecksumHelper import eu.opencloud.android.presentation.authentication.AccountUtils +import eu.opencloud.android.utils.MimetypeIconUtil import eu.opencloud.android.utils.NotificationUtils import eu.opencloud.android.utils.UPLOAD_NOTIFICATION_CHANNEL_ID import eu.opencloud.android.utils.RemoteFileUtils.getAvailableRemotePath @@ -360,7 +361,7 @@ class UploadFileFromContentUriWorker( private fun uploadDocument(client: OpenCloudClient) { val cacheFile = File(cachePath) - mimeType = cacheFile.extension + mimeType = MimetypeIconUtil.getBestMimeTypeByFilename(uploadPath) fileSize = cacheFile.length() ensureValidLastModified(null, cacheFile) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt index 3314368f8c..388d2be414 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt @@ -55,6 +55,7 @@ import eu.opencloud.android.lib.resources.files.CreateRemoteFolderOperation import eu.opencloud.android.lib.resources.files.UploadFileFromFileSystemOperation import eu.opencloud.android.lib.resources.files.tus.TusChecksumHelper import eu.opencloud.android.presentation.authentication.AccountUtils +import eu.opencloud.android.utils.MimetypeIconUtil import eu.opencloud.android.utils.NotificationUtils import eu.opencloud.android.utils.RemoteFileUtils.getAvailableRemotePath import eu.opencloud.android.utils.UPLOAD_NOTIFICATION_CHANNEL_ID @@ -182,7 +183,7 @@ class UploadFileFromFileSystemWorker( // Permissions not granted. Throw an exception to ask for them. throw LocalFileNotFoundException() } - mimetype = fileInFileSystem.extension + mimetype = MimetypeIconUtil.getBestMimeTypeByFilename(fileInFileSystem.name) fileSize = fileInFileSystem.length() ensureValidLastModified(fileInFileSystem) } From 018ff5b724ca6341882d3798cf6bfd13e89bba8a Mon Sep 17 00:00:00 2001 From: Markus Goetz Date: Thu, 11 Jun 2026 19:36:29 +0200 Subject: [PATCH 08/13] TUS: Also log actual retry error --- .../eu/opencloud/android/workers/TusUploadHelper.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt index 4abcf9b54e..f15ca170fa 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/TusUploadHelper.kt @@ -326,11 +326,15 @@ class TusUploadHelper( Timber.w("TUS: offset recovery failed (recovered=%s, current=%d)", recoveredOffset, offset) } - // Check if we've exhausted retries + // Check if we've exhausted retries. This retry loop is meant for transient + // network failures; local read errors (e.g. cache file deleted mid-upload) + // also end up here, but since the request body propagates them (PR #157) + // patchResult.exception carries the real cause — chain it so the final + // failure doesn't blame the network for a local problem. if (consecutiveFailures >= MAX_RETRIES) { throw java.io.IOException( - "TUS: giving up after $MAX_RETRIES retries at offset $offset (network error)", - IllegalStateException("TUS: max retries exceeded") + "TUS: giving up after $MAX_RETRIES retries at offset $offset", + patchResult.exception ?: IllegalStateException("TUS: max retries exceeded") ) } From c29df2c510b9038b4e4d12fe9296a8fb00bf0cb2 Mon Sep 17 00:00:00 2001 From: Markus Goetz Date: Fri, 12 Jun 2026 11:31:42 +0200 Subject: [PATCH 09/13] ETag: Don't overwrite in error case In case resolveFinalEtagIfNeeded fails. --- .../android/workers/UploadFileFromContentUriWorker.kt | 7 +++++-- .../android/workers/UploadFileFromFileSystemWorker.kt | 11 +++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt index 2342c9a0f8..661c9181c0 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromContentUriWorker.kt @@ -595,6 +595,9 @@ class UploadFileFromContentUriWorker( private fun updateFilesDatabaseWithLatestDetails() { val currentTime = System.currentTimeMillis() + // If the upload returned no etag and resolveFinalEtagIfNeeded() failed too, keep the + // existing etags instead of clobbering them with "" — a blank remoteEtag would also + // blank the thumbnail cache token. val serverEtag = FileEtagNormalizer.normalize(finalEtag).orEmpty() val file = getFileByRemotePathUseCase( GetFileByRemotePathUseCase.Params( @@ -607,8 +610,8 @@ class UploadFileFromContentUriWorker( val fileWithNewDetails = ocFile.copy( storagePath = null, needsToUpdateThumbnail = true, - etag = serverEtag, - remoteEtag = serverEtag, + etag = serverEtag.ifEmpty { ocFile.etag }, + remoteEtag = serverEtag.ifEmpty { ocFile.remoteEtag.orEmpty() }, length = fileSize, modificationTimestamp = lastModified.toLongOrNull()?.times(1000L) ?: currentTime, lastSyncDateForData = currentTime, diff --git a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt index cfb6813996..82aacac848 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/workers/UploadFileFromFileSystemWorker.kt @@ -467,6 +467,9 @@ class UploadFileFromFileSystemWorker( private fun updateFilesDatabaseWithLatestDetails() { val currentTime = System.currentTimeMillis() val getFileByRemotePathUseCase: GetFileByRemotePathUseCase by inject() + // If the upload returned no etag and resolveFinalEtagIfNeeded() failed too, keep the + // existing etags instead of clobbering them with "" — a blank remoteEtag would also + // blank the thumbnail cache token. val serverEtag = FileEtagNormalizer.normalize(finalEtag).orEmpty() val file = getFileByRemotePathUseCase(GetFileByRemotePathUseCase.Params(account.name, uploadPath, ocTransfer.spaceId)) file.getDataOrNull()?.let { ocFile -> @@ -474,8 +477,8 @@ class UploadFileFromFileSystemWorker( if (ocTransfer.forceOverwrite) { ocFile.copy( needsToUpdateThumbnail = true, - etag = serverEtag, - remoteEtag = serverEtag, + etag = serverEtag.ifEmpty { ocFile.etag }, + remoteEtag = serverEtag.ifEmpty { ocFile.remoteEtag.orEmpty() }, length = fileSize, lastSyncDateForData = currentTime, modifiedAtLastSyncForData = currentTime, @@ -485,8 +488,8 @@ class UploadFileFromFileSystemWorker( ocFile.copy( storagePath = null, needsToUpdateThumbnail = true, - etag = serverEtag, - remoteEtag = serverEtag, + etag = serverEtag.ifEmpty { ocFile.etag }, + remoteEtag = serverEtag.ifEmpty { ocFile.remoteEtag.orEmpty() }, length = fileSize, lastSyncDateForData = currentTime, modifiedAtLastSyncForData = currentTime, From 1d6eda5a8c97951ee31c7cc82506b1aebf678244 Mon Sep 17 00:00:00 2001 From: Markus Goetz Date: Fri, 12 Jun 2026 12:29:02 +0200 Subject: [PATCH 10/13] Add values-PLEASE_USE_TRANSIFEX_FOR_TRANSLATIONS.txt --- .../main/res/values-PLEASE_USE_TRANSIFEX_FOR_TRANSLATIONS.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 opencloudApp/src/main/res/values-PLEASE_USE_TRANSIFEX_FOR_TRANSLATIONS.txt diff --git a/opencloudApp/src/main/res/values-PLEASE_USE_TRANSIFEX_FOR_TRANSLATIONS.txt b/opencloudApp/src/main/res/values-PLEASE_USE_TRANSIFEX_FOR_TRANSLATIONS.txt new file mode 100644 index 0000000000..b1ff23feba --- /dev/null +++ b/opencloudApp/src/main/res/values-PLEASE_USE_TRANSIFEX_FOR_TRANSLATIONS.txt @@ -0,0 +1,2 @@ +-> https://app.transifex.com/opencloud-eu/opencloud-eu/android/ + From b3801edfa603f5d758e322af929662e2bc69a845 Mon Sep 17 00:00:00 2001 From: Markus Goetz Date: Fri, 12 Jun 2026 12:29:40 +0200 Subject: [PATCH 11/13] build.gradle: 1.2.4 --- opencloudApp/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opencloudApp/build.gradle b/opencloudApp/build.gradle index 41fa9a9cf2..1debe35a74 100644 --- a/opencloudApp/build.gradle +++ b/opencloudApp/build.gradle @@ -119,7 +119,7 @@ android { testInstrumentationRunner "eu.opencloud.android.utils.OCTestAndroidJUnitRunner" versionCode = 10 - versionName = "1.3.0" + versionName = "1.2.4" buildConfigField "String", gitRemote, "\"" + getGitOriginRemote() + "\"" buildConfigField "String", commitSHA1, "\"" + getLatestGitHash() + "\"" From 11e4b7cbb8dc08a49a8997153e78978d735694b7 Mon Sep 17 00:00:00 2001 From: Markus Goetz Date: Fri, 12 Jun 2026 12:43:39 +0200 Subject: [PATCH 12/13] Pull latest translations, including IT, ES For release and https://github.com/opencloud-eu/android/pull/188 --- .../src/main/res/values-de/strings.xml | 26 +- .../src/main/res/values-es/strings.xml | 854 ++++++++++++++++++ .../src/main/res/values-fr/strings.xml | 17 +- .../src/main/res/values-it/strings.xml | 848 +++++++++++++++++ .../src/main/res/values-nl/strings.xml | 54 +- .../src/main/res/values-pl/strings.xml | 575 +++++++++++- 6 files changed, 2320 insertions(+), 54 deletions(-) create mode 100644 opencloudApp/src/main/res/values-es/strings.xml create mode 100644 opencloudApp/src/main/res/values-it/strings.xml diff --git a/opencloudApp/src/main/res/values-de/strings.xml b/opencloudApp/src/main/res/values-de/strings.xml index 152fdf480b..7ffe960e1d 100644 --- a/opencloudApp/src/main/res/values-de/strings.xml +++ b/opencloudApp/src/main/res/values-de/strings.xml @@ -136,10 +136,10 @@ Die Authentifizierung ist fehlgeschlagen. Melden Sie sich erneut an, um wieder Zugriff zu erhalten. Ihre Serverversion ist niedriger als 10 und wird daher nicht unterstützt. Bitte aktualisieren Sie Ihren Server auf eine neuere Version. Konto wechseln - Das Zugriffstoken ist abgelaufen oder ungültig geworden. Melden Sie sich erneut an, um wieder Zugriff zu erhalten. + Der Zugriffstoken ist abgelaufen oder ungültig geworden. Melden Sie sich erneut an, um wieder Zugriff zu erhalten. Anmelden Server überprüfen - Serveradresse https://&#8230; + Serveradresse https://… Benutzername Passwort Neu bei %1$s\? @@ -172,7 +172,7 @@ Sie haben aktuell keinen Zugriff auf einen Space! Dateien und Ordner, die Sie als offline verfügbar markieren, werden hier angezeigt. Dateien und Ordner, die Sie per Link teilen, werden hier angezeigt. - &#8230; wird geladen + Lädt… Keine App für diesen Dateityp gefunden Keine App für diese Aktion gefunden Es befinden sich keine Dateien in diesem Ordner. @@ -210,7 +210,7 @@ Zurück Speichern & Verlassen Fehler - &#8230; wird geladen + Lädt… unbekannt unbekannter Fehler Ausstehend @@ -220,7 +220,7 @@ Konto erstellen Hochladen von Ordnername - &#8230; wird hochgeladen + Hochladen… %1$d%%%2$s wird hochgeladen Hochladen erfolgreich %1$s hochgeladen @@ -270,7 +270,7 @@ Hochladen abgebrochen Herunterladen abgebrochen Der Download von %1$s wurde in die Warteschlange eingereiht. - &#8230; wird heruntergeladen + Herunterladen… %1$d%% %2$s wird heruntergeladen Herunterladen erfolgreich %1$s heruntergeladen @@ -344,8 +344,8 @@ Vorspultaste Bild von der Kamera - Autorisierung wird abgerufen &#8230; - Anmeldeversuch &#8230; + Autorisierung wird abgerufen… + Anmeldeversuch… Keine Netzwerkverbindung Sichere Verbindung nicht verfügbar. Verbindung hergestellt @@ -396,7 +396,7 @@ Der Ordner existiert bereits. Eine Datei mit dem Namen %1$s existiert bereits. Ein Ordner mit dem Namen %1$s existiert bereits. - Externe Datei konnte nicht überprüft werden + Nicht lokale Datei konnte nicht überprüft werden Dateiinhalte sind bereits synchronisiert Datei nicht gefunden Eine neue Version wurde auf dem Server gefunden. Wird heruntergeladen… @@ -523,7 +523,7 @@ Verlauf senden Keine App zum Versenden von Protokollen gefunden. Bitte installieren Sie eine E-Mail-App. %1$s Android-App-Protokolle - data&#8230; wird geladen + Daten werden geladen… Alle auswählen Auswahl umkehren @@ -613,7 +613,7 @@ Nach Personen und Gruppen suchen %1$s (Gruppe) - %1$s (extern) + %1$s (nicht lokal) %1$s ( auf %2$s ) Entschuldigung, Ihre Serverversion erlaubt keine Freigaben für Benutzer innerhalb von Clients.\nBitte wenden Sie sich an Ihren Administrator. @@ -708,7 +708,7 @@ Es wurden %1$s an Daten auf Ihrem externen Speicher gefunden. Diese werden in den sicheren Speicher auf Ihrem Gerät verschoben. Die verbleibenden Dateien werden nach der Migration von Ihrem externen Speicher entfernt, um Duplikate und Sicherheitslücken zu vermeiden. Ihre App wurde aktualisiert. Dieses App-Update verschiebt Ihre Dateien an einen sichereren Speicherort auf Ihrem Gerät. Diese einmalige Datenmigration ist erforderlich und kann bis zu einer Minute oder länger dauern. Los geht’s - &#9888;️ Der freie Speicherplatz auf Ihrem Gerät ist derzeit begrenzt. Einige Dateien werden möglicherweise nicht migriert. Bitte überprüfen Sie, ob alle heruntergeladenen Dateien nach Abschluss des Vorgangs weiterhin heruntergeladen sind. + ⚠️ Der freie Speicherplatz auf Ihrem Gerät ist derzeit begrenzt. Möglicherweise werden nicht alle Dateien migriert. Bitte überprüfen Sie nach Abschluss des Vorgangs, ob alle heruntergeladenen Dateien weiterhin lokal gespeichert sind. Jetzt migrieren Migration erfolgreich abgeschlossen. Ihre Dateien sind jetzt sicherer denn je. Auf Ihre Dateien zugreifen @@ -729,7 +729,7 @@ Neu in %1$s - Vielen Dank, dass Sie %1$s nutzen.\n&#10084; + Vielen Dank, dass Sie %1$s verwenden.\n♥️ Fortfahren Symbol für Release-Notes Kleinere Fehlerbehebungen diff --git a/opencloudApp/src/main/res/values-es/strings.xml b/opencloudApp/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000..6066711142 --- /dev/null +++ b/opencloudApp/src/main/res/values-es/strings.xml @@ -0,0 +1,854 @@ + + + + + Nombre + Fecha + Tamaño + Buscar aquí + Buscar en Espacios + Sincronizar cuenta + Subir + Subir desde otras aplicaciones + Archivos + Abrir con + Abrir con (solo lectura) + Nueva carpeta + Ajustes + Detalles + Enviar + Descendente + Ordenar por + Nuevo documento + Acceso directo + Todos los archivos + Sin conexión + Enlaces compartidos + Ajustes + Subidas + Cerrar + Abrir + Cargando cuota + %1$s de %2$s usados (%3$s %%) + Espacio lleno + Queda poco espacio + Queda muy poco espacio + %1$s en uso + Información de almacenamiento usado no disponible + GitHub - Sugerencias + Ayuda + General + Registros + Más + Seguridad + Novedades + Configurar el bloqueo de acceso y permitir pulsaciones con otras ventanas visibles + Seguimiento de los registros de la aplicación + Información, sugerencias y más + Cuentas + Administrar cuentas + Código PIN + Bloqueo biométrico + Activa Código PIN o Patrón para habilitar esta opción + Bloquear aplicación + Inmediatamente + Después de 1 minuto + Después de 5 minutos + Después de 30 minutos + Bloquear acceso desde administrador de archivos + Bloquear el acceso de otras aplicaciones a los archivos de las cuentas a través del administrador de archivos de Android. + Bloquear pulsaciones con aplicaciones superpuestas + Permite interactuar con la aplicación aunque haya otras ventanas visibles por encima. Activa para usar aplicaciones de atenuación de pantalla. + ¿Seguro que quieres activar esta función\? + Utiliza esta función bajo tu responsabilidad. Una aplicación maliciosa podría intentar engañarte para que realices acciones sin darte cuenta a través de otras pantallas superpuestas. + Subida automática de imágenes + Gestionar ubicación y comportamiento de las imágenes subidas automáticamente. + Subida automática de vídeos + Gestionar ubicación y comportamiento de los vídeos subidos automáticamente. + Subida de imágenes + Subir fotos de la cámara automáticamente + Carpeta de subida de imágenes + Condiciones para las subidas + Las subidas se iniciarán cuando se cumplan todas las opciones seleccionadas. + Subir imágenes solo por wifi + Subir imágenes solo durante la carga + Habilitar subida solo por wifi para activar esta opción + Subida de videos + Subir vídeos de la cámara automáticamente + Carpeta de subida de vídeos + Subir vídeos solo por wifi + Subir vídeos solo durante la carga + Las subidas automáticas no están disponibles para usuarios Light. + Avanzado + Gestionar notificaciones + Mostrar archivos ocultos + Archivos de registro + Sin archivos de registro + Activar el registro para mostrar aquí los archivos. + Activar registro + Se usa para registrar problemas. + Historial de registro + Muestra los registros guardados. + Borrar historial + Ayuda + Sincronizar contactos, calendarios y tareas + Instalar DAVx⁵ + Abrir gestor de archivos + Aplicación recomendada para examinar archivos mediante el explorador nativo de Android + Recomendar a un amigo + Comentarios + Aviso legal + Recordar ubicación de subida + Recuerda la última carpeta de subida + Bloqueo por patrón + Activar registro + Al activarse, los registros pueden afectar al rendimiento e incluir información confidencial. No se envían automáticamente a los servidores de OpenCloud. Compartirlos es responsabilidad exclusiva del usuario. + Enviar comentarios + Versión de la aplicación + %1$s %2$s versión %3$s (%4$s) + Introducir patrón + Introduce el patrón + Eliminar patrón + Patrón incorrecto + Confirma el patrón. + Los patrones no coinciden. + Error al configurar el bloqueo por patrón. + Error al eliminar el patrón de bloqueo. + No se pueden activar el patrón y el código PIN a la vez. Desactiva primero el patrón. + No se pueden activar el patrón y el código PIN a la vez. Desactiva primero el código PIN. + Ya no se solicitará el patrón. + Se solicitará el patrón cada vez que se abra la aplicación. + Bloqueado + Se requiere una ACTION válida en el intent enviado a + + + Archivos + Personal + Subidas + Sin conexión + Enlaces + Compartidos + + ¡Prueba %1$s en tu móvil! + ¡Te invito a usar %1$s en tu móvil!\nDescárgalo aquí: %2$s + + porque + Error de autenticación. Inicia sesión de nuevo para volver a entrar. + La versión de tu servidor es anterior a la 10 y no es compatible. Actualiza el servidor a una versión más reciente. + Cambiar de cuenta + El token de acceso ha caducado o no es válido. Inicia sesión de nuevo para volver a entrar. + Iniciar sesión + Comprobar servidor + Dirección del servidor https://… + Nombre de usuario + Contraseña + ¿Nuevo en %1$s\? + Archivos + Iniciar sesión + Subir + Elegir carpeta de subida + Elegir Espacio de subida + Cuenta no encontrada + No hay cuentas de %1$s en tu dispositivo. Crea una cuenta primero. + Configurar + Salir + No hay archivos para subir + %1$s no puede subir texto como un archivo. + El archivo recibido no es válido. + El archivo no se pudo subir + %1$s no tiene los permisos para leer el archivo. + No se encontró el archivo para subir. Comprueba si el archivo existe. + Ocurrió un error al copiar el archivo a una carpeta temporal. Intenta enviarlo de nuevo + hace unos segundos + Sin archivos + Sin Espacios + Espacio: + No hay archivos sin conexión + Sin enlaces compartidos + Sube contenido o sincroniza tus dispositivos. + Espacio de usuario no disponible + No hay elementos compartidos + No participas en recursos de otros usuarios. + No tienes acceso a ningún Espacio. + Los archivos y carpetas que marques para usar sin conexión aparecerán aquí. + Los archivos y carpetas que compartas mediante un enlace aparecerán aquí. + Cargando... + No se encontró ninguna aplicación para este tipo de archivo + No se encontró ninguna aplicación para esta acción + La carpeta está vacía. + No hay carpetas. + Sin resultados + No hay subidas disponibles. + Sube algo y aparecerá aquí. + carpeta + carpetas + archivo + archivos + Toca un archivo para mostrar información adicional. + Subir a %1$s + Selecciona el tipo de documento a crear: + Archivos + Tamaño: + Tipo: + Creado: + Sincronizado: + Ruta: + Modificado: + Descarga + Imagen de archivo + Cancelar descarga + Sincronizar + El archivo se renombró a %1$s durante la subida + Vista de lista + Compartir + Preparando la cuenta para el primer uso + + No + Eliminar subida + Reintentar subida + Cancelar sincronización + Atrás + Guardar y salir + Error + Cargando... + desconocido + error desconocido + Pendiente + Importante + Cambiar contraseña + Eliminar cuenta + Crear cuenta + Subir desde + Nombre de la carpeta + Subiendo... + %1$d%% Subiendo %2$s + Subida completada + %1$s subido + Subida fallida + La subida de %1$s no se pudo completar. + Subida fallida, inicia sesión de nuevo. + El certificado del servidor no es de confianza. + Introduce un nombre para el archivo. + El nombre del archivo no puede estar vacío. + El nombre no puede tener más de%d caracteres. + Nombre del archivo + Subiendo archivos de la cámara + Se subirán %d fotos nuevas. + Se subirán %d vídeos nuevos. + Fallo en las subidas de la cámara. + La ruta de origen de las fotos ya no es válida. + La ruta de origen de los vídeos ya no es válida. + Subiendo archivos sin conexión disponibles + Subiendo archivos solicitados por wifi + El certificado del servidor no es de confianza. + Subidas + En curso + Error (pulsa para reintentar) + Subido + En cola + %d archivos + %d archivo + Completado + Cancelado + Pausado + Error de conexión + La subida se reintentará en breve + Error de credenciales + Error de carpeta + Error de archivo + Archivo local no encontrado + Error de permisos + Prohibido por una regla de cortafuegos + Conflicto + La aplicación se cerró + Error desconocido + Esperando conexión wifi + En cola + Esperando para subir + Tipo de archivo no admitido + La subida de %1$s se ha puesto en cola + Subida cancelada + Descarga cancelada + La descarga de %1$s se ha puesto en cola + Descargando... + %1$d%% Descargando %2$s + Descarga completada + %1$s descargado + Descarga fallida + No se puede mostrar el archivo + La descarga de %1$s no se pudo completar + Aún no se ha descargado + Descarga fallida, inicia sesión de nuevo. + para descargar este archivo + Seleccionar cuenta + Sincronización fallida + Sincronización fallida, inicia sesión de nuevo. + La sincronización de %1$s no se pudo completar. + Contraseña incorrecta para %1$s + Conflictos detectados + Fallo al mantener sincronizados %1$d archivos + Mantener archivos sincronizados ha fallado + El contenido de %1$d archivos no pudo ser sincronizado (%2$d conflictos) + La carpeta %1$s ya no existe. + + Introduce tu PIN. + Configura tu PIN. + Introduce un PIN de %1$d-dígitos. + El Se solicitará el PIN cada vez que se abra la aplicación. + Confirma el PIN. + Eliminar PIN. + Los PIN no coinciden. + PIN incorrecto + Error al configurar el bloqueo por PIN. + Error al eliminar el bloqueo por PIN. + Eliminar + Identificación biométrica + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + + Reintentar en %1$s + + Registra al menos un elemento biométrico para usar esta función. + Usar autenticación biométrica + Iniciar sesión usando la autenticación biométrica. + ¿Deseas activar la autenticación biométrica\? + Autenticación biométrica no disponible, el sistema no la pudo proporcionar + + %1$s reproductor de musica + %1$s (reproduciendo) + %1$s (cargando) + %1$s reproducción finalizada + No se encontró ningún archivo multimedia + No se ha añadido ninguna cuenta + El archivo no pertenece a una cuenta válida + Códec no compatible + El archivo multimedia no se puede leer + Archivo multimedia mal codificado + Se agotó el tiempo al intentar reproducir + No se puede reproducir el archivo multimedia en línea + El reproductor actual no puede reproducir el archivo + Error de seguridad al reproducir %1$s + Error de lectura al reproducir %1$s + Error inesperado al reproducir %1$s + Rebobinar + Reproducir o pausar + Avance rápido + Foto de la cámara + + Obteniendo autorización + Intentando iniciar sesión... + Sin conexión de red + Conexión segura no dispoble + Conexión establecida + Probando conexión + Configuración del servidor incorrecta + Una cuenta para el mismo usuario y servidor ya existe en el dispositivo. + El usuario introducido no coincide con el de esta cuenta. + ¡Error desconocido! + No se encontró el servidor + Formato de dirección del servidor incorrecto + Fallo al iniciar SSL + Versión del servidor no reconocida + No se pudo establecer la conexión + Conexión segura establecida + Usuario o contraseña incorrectos + Error de autorización + Acceso denegado por el servidor de autorización + Error inesperado; por favor, introduce la dirección del servidor de nuevo + La autorización ha caducado. Por favor, inténtalo de nuevo. + Introduce la contraseña + Conectando con el servidor de autenticación... + El método de autenticación no es compatible con el servidor + El servidor no devuelve un ID de usuario correcto. Por favor, contacta con un administrador. + No se pudo autenticar en este servidor + La cuenta aún no existe en el dispositivo + + Marcar como disponible sin conexión + Desmarcar como disponible sin conexión + El archivo marcado como disponible sin conexión + Archivo desmarcado como disponible sin conexión + Renombrar + Eliminar + ¿Seguro que quieres eliminar %1$s\? + ¿Seguro que quieres eliminar %1$s y todo su contenido\? + Solo local + ¿Seguro que quieres eliminar la cuenta %1$s\? + Esta cuenta tiene vinculada la subida desde la cámara, eliminar la cuenta deshabilitará esta opción. ¿Seguro que quieres eliminar la cuenta %1$s\? + Eliminar enlace + Eliminar recurso compartido privado + ¿Seguro que quieres eliminar %1$s compartido\? + Eliminado correctamente + Error al eliminar + Introduce un nombre nuevo + Una carpeta que contiene este archivo está disponible sin conexión. + No se pudo completar el cambio de nombre; prueba uno diferente + No se pudo completar el cambio de nombre + El archivo ya existe + La carpeta ya existe + Ya existe un archivo con el nombre %1$s. + Ya existe una carpeta con el nombre %1$s. + No se pudo comprobar el archivo remoto + Contenido del archivo ya sincronizado + Archivo no encontrado + Se ha encontrado una nueva versión en el servidor. Descargando... + Descarga en cola + Subida en cola + No se pudo crear la carpeta + No se pudo crear el archivo + Caracteres prohibidos: / \\ + El nombre del archivo contiene al menos un carácter no válido + El nombre no puede estar vacío + El nombre no puede ser tan largo + Espera un momento + Comprobando credenciales guardadas + Error inesperado; selecciona el archivo desde otra aplicación + No se ha seleccionado ningún archivo + Enviar enlace a + Copiando archivo desde almacenamiento privado + + Iniciar sesión con oAuth2 + Conectando al servidor OAuth2… + + La conexión no es segura, no se permite el tráfico http. + No se pudo verificar la identidad del servidor. + - El certificado del servidor no es de confianza. + - El certificado del servidor ha caducado. + - Las fechas de validez del certificado del servidor son futuras. + - La URL no coincide con el nombre del servidor en el certificado + ¿Confiar en este certificado de todas formas\? + No se pudo guardar el certificado. + Detalles + Oculto + Emitido para: + Emitido por: + Nombre común: + Organización: + Unidad organizativa: + País: + Estado: + Localidad: + Validez: + Desde: + Hasta: + Firma: + Algoritmo: + Este algoritmo de hash no está disponible en tu teléfono. + Huella digital: + Hubo un problema al cargar el certificado. + No se pudo mostrar el certificado. + - No hay información sobre el error + + Texto de relleno + placeholder.txt + Imagen PNG + 389 KB + 2012/05/18 12:23 PM + 12:23:45 + + Confirmar + ¿Seguro que quieres desactivar esta función\? Las fotos pendientes no se subirán. + ¿Seguro que quieres desactivar esta función\? Los vídeos pendientes no se subirán. + Asegúrate de que la carpeta seleccionada sea donde la cámara guarda las fotos. De lo contrario, no podrán ser detectadas. Ten en cuenta que se subirán al menos 15 minutos después de haberlas hecho. + Asegúrate de que la carpeta seleccionada sea donde la cámara guarda los vídeos. De lo contrario, no podrán ser detectados.. Ten en cuenta que se subirán al menos 15 minutos después de haberlos hecho. + Archivo en conflicto + Hay un conflicto en el archivo %1$s, pulsa para solucionarlo. + ¿Que archivos deseas mantener\? Si seleccionas ambas versiones, se añadirá un número al nombre del archivo local. + Mantener ambas + versión local + versión de servidor + Ocurrió un error en la carpeta de destino. + Reemplazar + + Previsualización de imagen + No se puede mostrar esta imagen. + + No se pudo copiar %1$s a la carpeta local %2$s. + + No fue posible obtener los elementos compartidos del servidor. + No fue posible obtener los usuarios y grupos del servidor. + No fue posible obtener las capacidades del servidor. + Lo sentimos, la función de compartir no está habilitada en tu servidor. Por favor, contacta con tu administrador. + No fue posible compartir este elemento. + No fue posible dejar de compartir este elemento. + No fue posible actualizar este elemento. + Introduce una contraseña + Debes introducir una contraseña. + + Enviar + + Copiar enlace + Copiado al portapapeles + No se recibió texto para copiar al portapapeles + Error inesperado al copiar al portapapeles + Texto copiado de %1$s + + Error crítico: no se pudieron completar las acciones + + Ocurrió un error al conectar con el servidor + El servidor tardó demasiado en responder. + El servidor tardó demasiado en conectarse. + No se pudo establecer conexión con el servidor. + Ocurrió un error en la red. + El recurso está bloqueado + + No tienes permiso %s + + para renombrar el archivo + para borrar el archivo + para compartir el archivo + para dejar de compartir el archivo + para actualizar el elemento compartido + para crear el archivo + para subir archivos en esta carpeta + Subida no permitida + El archivo ya no está disponible en el servidor. + + Cuentas + Añadir cuenta + Administrar cuentas + %1$s de %2$s + Usado: + La conexión segura se ha redirigido a través de una ruta no segura. + + Registros + Enviar historial + No se encontró una aplicación para enviar los registros. Instale una aplicación de correo. + Registros de %1$s en Android + Cargando datos... + + Seleccionar todo + Invertir selección + + Mover + Aquí no hay nada. ¡Puedes añadir una carpeta! + Escoger + Mover aquí + Copiar aquí + ¡No tienes permiso para añadir cosas aquí! + + No se pudo mover. Comprueba si el archivo existe. + No se puede mover una carpeta dentro de su propia subcarpeta. + El archivo ya existe en la carpeta de destino. + Ocurrió un error al mover este archivo o carpeta. + para mover este archivo + + No se pudo copiar. Compruebe si el archivo existe. + No se puede copiar una carpeta dentro de su propia subcarpeta. + El archivo ya existe en la carpeta de destino. + Ocurrió un error al copiar este archivo o carpeta. + para copiar este archivo + + Subidas de la cámara + + No se pudo completar la sincronización de la carpeta %1$s + + compartido + contigo + + %1$s ha compartido \"%2$s\" contigo + Han compartido \"%1$s\" contigo + + Volver a conectar + Dirección del servidor + Espacio insuficiente + + Nombre de usuario + + 1 carpeta + %1$d carpetas + 1 archivo + 1 archivo, 1 carpeta + 1 archivo, %1$d carpetas + %1$d archivos + %1$d archivos, 1 carpeta + %1$d archivos, %2$d carpetas + Cuenta para subir imágenes + Cuenta para subir vídeos + Carpeta de la cámara (%1$s) + necesario + El archivo original + El archivo original + Última sincronización + Puedes cambiar tus preferencias en Ajustes + Copiar archivo + Mover archivo + + se conservará en la carpeta original + se eliminará de la carpeta original + + Compartir + Compartir %1$s + Usuarios y Grupos + Aún no se ha compartido nada con ningún usuario + Para compartir con otros usuarios, el servidor debe estar al menos en la versión 8.2 + Añadir usuario o grupo + Enlaces públicos + Crear enlace para compartir + Editar enlace para compatir + Nombre del link + Cancelar + Guardar + Caducidad + Contraseña + Proteger con contraseña + Generar + Copiar + Descargar/Ver + Descargar/Ver/Subir + Solo subir (Buzón de archivos) + Crear enlace + Compartir con + Compartir con %1$s + + Buscar + + Buscar usuarios y grupos + %1$s (grupo) + %1$s (remoto) + %1$s ( en %2$s ) + + Lo sentimos, la versión de tu servidor no permite compartir con otros usuarios desde la aplicación. Ponte en contacto con tu administrador. + puede compartir + puede editar + crear + cambiar + eliminar + Dejar de compartir + hecho + + Reintentar + Limpiar + + No se ha encontrado el archivo en el sistema de archivos local + ¿Seguro que quieres eliminar estos%s elementos\? + ¿Seguro que quieres eliminar los elementos seleccionados y su contenido\? + No hay suficiente espacio en la cuenta + Autenticación denegada + + Política de privacidad + Política de privacidad + Ha ocurrido un error: + El servidor no está disponible. + + No se puede iniciar la transmisión porque el certificado de tu servidor no es de confianza. La descarga del archivo comenzará automáticamente. + Este formato de vídeo no es compatible. + Este archivo de vídeo ya no está disponible en el servidor. + No se permite la transmisión a través de redirecciones cruzadas. + No se puede reproducir este vídeo. + + La vista previa de vídeo no es compatible con tu versión de Android. La descarga del archivo comenzará automáticamente. + \"Posición no disponible\" + El dispositivo no está conectado a una red. + Aún no se han creado enlaces públicos. + Este recurso compartido no tiene enlace. + Cualquier persona que tenga el enlace tiene acceso al archivo/carpeta. + ***** + Contraseña * + Caducidad * + El enlace publico caducará %1$d días de su creación. + Enlace %1$s + La contraseña está vacía. + Enlace privado: solo las personas que tengan acceso al archivo o carpeta podrán usarlo. Utilízalo como un enlace permanente para ti o para dirigir a otros usuarios a archivos dentro de recursos compartidos. + El enlace privado no está disponible para este archivo. + Descargas + Ver el nombre del archivo y el progreso de la descarga. + Subidas + Ver el nombre del archivo y el progreso de la subida. + Reproductor de música + Mostrar el reproductor de música + Sincronización de archivos + Ver el resultado de la sincronización de archivos. + Conflictos de archivos + Ver los conflictos de archivos cuando ocurran. + Estás a %1$d clicks activar los menús de desarrollador + + ¡Valora la aplicación %1$s! + Si te gusta usar esta aplicación, ¿podrías dedicar un momento a valorarla\? Tus comentarios son muy importantes para nosotros.  + NO, GRACIAS + MÁS TARDE + VALORAR AHORA + + %d seleccionado + %d seleccionados + %d seleccionados + + + + Saltar + Gestiona todos tus archivos sincronizados + Puedes copiar, mover y eliminar + Comparte archivos y carpetas + Puedes compartir de forma privada o pública + Multicuenta + Conéctate a todos tus servidores de OpenCloud + Subidas de la cámara + Tus fotos/vídeos se subirán automáticamente + Transmisión de vídeo + Reproduce tus vídeos sin necesidad de descargarlos + No hay ningún navegador instalado. Por favor, instala uno para poder iniciar sesión de forma segura. + no se ha encontrado + + No fue posible determinar si OAuth2 es compatible. + No fue posible determinar la URL base del servidor. + Registrar peticiones y respuestas HTTP + Los registros pueden contener información confidencial. Compartir los registros con terceros es responsabilidad exclusiva del usuario + + Sincronizando cuenta + No se pudieron actualizar los Espacios + + + Se han encontrado %1$s de datos en tu almacenamiento externo. Se moverán al almacenamiento seguro de tu dispositivo. Los archivos restantes se eliminarán de tu almacenamiento externo después de la migración para evitar duplicados y vulnerabilidades. + Tu aplicación se ha actualizado. Esta actualización de la aplicación migra tus archivos a una ubicación más segura en tu dispositivo. Esta migración de datos única es necesaria y puede tardar hasta un minuto o más. + Empecemos + ⚠️ Tu espacio libre actual en el dispositivo es limitado. Es posible que algunos archivos no se migren; por favor, comprueba que todos los archivos descargados sigan estándolo una vez completado el proceso. + Migrar ahora + Migración completada con éxito. Tus archivos están más seguros que nunca. + Accede a tus archivos + Migrando tus archivos. Por favor, no apagues el dispositivo. + Trabajando... Por favor, espera + Más seguridad para tus archivos + + + Por favor, selecciona una opción para bloquear la aplicación: + Código PIN + Bloqueo por patrón + + Sube contenido o sincroniza tus dispositivos. + + + Continuar + Nombre de usuario vacío + + + Nuevo en %1$s + Gracias por usar %1$s.\n♥️ + Continuar + Icono de notas de la versión + Corrección de errores menores + Se han corregido varios errores para mejorar la experiencia en la aplicación + Avisos de disponibilidad sin conexión en vistas previas + Se ha añadido un aviso al activar o desactivar la disponibilidad sin conexión en todas las vistas previas y se ha actualizado el menú de opciones según el estado del archivo + Mejoras en la visualización del espacio ocupado + El espacio de almacenamiento se actualiza de forma frecuente durante las operaciones con archivos y ahora está disponible en la vista del gestor de cuentas, además de en el menú lateral. + Usuarios Light + La aplicación ya es compatible con los usuarios Light (usuarios sin espacio personal). + Corrección en la creación de archivos mediante proveedores de la aplicación + Los archivos ahora se abren correctamente cuando se crean a través de los proveedores de la aplicación. + OpenCloud Client + Te damos la bienvenida a la versión inicial + + + + Abrir en la web + Abrir en %1$s (web) + No se pudo abrir en la web + No hay aplicaciones que admitan este tipo de archivo. + El archivo solicitado aún no está disponible; inténtalo de nuevo más tarde. + + + Aviso de la cuenta + Por favor, salga de la cuenta y vuelva a iniciar sesión para activar la función de Espacios. + No volver a mostrar + Entendido + + Texto + + Aplicar a todos los %1$s conflictos + Archivo de registro descargado + Ir a Descargas + El archivo de registro %1$s se ha descargado correctamente en la carpeta Descargas. + + + Formato de enlace no válido + El usuario no tiene acceso al archivo + Abriendo el archivo desde el enlace + + Recargar + Sincronización ya en curso + + Al menos %1$d caracteres + Máximo de %1$d caracteres + Al menos %1$d números + Al menos %1$d minúsculas + Al menos %1$d mayúsculas + Introduzca una contraseña que cumpla con los siguientes criterios:\n + Al menos %1$d de los siguientes caracteres especiales: \"%2$s\" + Por desgracia, su contraseña es muy común. Por su seguridad, elija una contraseña más difícil de adivinar. + + Nunca + 1 hora + 12 horas + 1 día + 30 días + Borrar copias locales + Eliminar automáticamente archivos descargados que no estén disponibles sin conexión, cuando el tiempo seleccionado haya transcurrido desde su último uso.\nTiempo seleccionado: %1$s + + HTTP URL insegura + La URL introducida utiliza el protocolo HTTP en lugar del protocolo cifrado HTTPS. Si continúas, la comunicación no estará cifrada. + Continuar + + Se eliminarán todos los archivos descargados que no estén disponibles sin conexión para la cuenta %1$s + Eliminar almacenamiento local + Borrar datos + + Menú + Administrar cuentas + Buscar + Tipo de vista + Imagen previsualizada + Añadir enlace público + Obtener enlace privado + Obtener enlace público + Eliminar enlace público + Editar enlace público + Eliminar cuenta + Añadir nuevo contenido + Añadir nuevo contenido expandido + Limpiar almacenamiento de la cuenta + Logotipo + Añadir compartido + Editar compartido + Eliminar compartido + %1$s operaciones + Ordenar por %1$s ascendente + Ordenar por %1$s descendente + Crear nueva carpeta + + Crear acceso directo + URL + La URL debe empezar por https:// o http:// para que se pueda abrir en el navegador. + La URL no puede contener espacios en blanco. + .url + Este acceso directo apunta a: + Abrir enlace + + repositorio de GitHub]]> + + Enlace + Botón + + Selector de carpetas + PIN + Patrón + Recibir archivos externos + Previsualización de video + Notas de la versión + Previsualización de imagen + Iniciar sesión + Novedades + Vista previa de audio + Detalles + Vista previa de texto + Etiquetas de texto añadidas en la barra inferior + Se han añadido etiquetas de texto y se utiliza el indicador activo predeterminado para mostrar qué sección está seleccionada en la barra inferior. + + diff --git a/opencloudApp/src/main/res/values-fr/strings.xml b/opencloudApp/src/main/res/values-fr/strings.xml index e4f840a1dd..c4b4881544 100644 --- a/opencloudApp/src/main/res/values-fr/strings.xml +++ b/opencloudApp/src/main/res/values-fr/strings.xml @@ -58,7 +58,7 @@ Verrouiller l’accès au gestionnaire de fichier Verrouiller l\'accès des autres applications aux fichiers d\'utilisateurs de l\'application via le navigateur de fichiers natif d\'Android. Interactions avec d\'autres fenêtres visibles - Autoriser les interactions quand une autre fenêtre visible se superpose a l\'affichage. Activer pour utiliser le fonctionnement des applications de filtre lumineux. + Autoriser les interactions quand une autre fenêtre visible se superpose à l\'affichage. Activer pour utiliser le fonctionnement des applications de filtre lumineux. Êtes-vous sûr de vouloir activer cette fonctionnalité \? Utilisez cette fonctionnalité à vos propres risques. Une application malveillante pourrait tenter de vous pousser à effectuer des actions en falsifiant l\'affichage. Téléversement automatique des images @@ -78,7 +78,7 @@ Chemin d\'accès au répertoire de téléversement des vidéos Téléverser les images via une connexion WiFi uniquement Téléverser les vidéos uniquement pendant le chargement de la batterie - Les téléversements automatiques ne sont pas disponibles pour les utilisateurs légers + Les téléversements automatiques ne sont pas disponibles pour les utilisateurs Light Avancé Gérer les notifications Afficher les fichiers cachés @@ -139,7 +139,6 @@ Le jeton d\'accès de la connexion a expiré ou est devenu invalide. Connectez-vous à nouveau pour rétablir l\'accès. Se connecter Vérifier le serveur - Adresse du serveur https://… Nom d\'utilisateur Mot de passe Nouveau dans %1$s \? @@ -220,7 +219,6 @@ Créer un compte Téléversez depuis Nom du dossier - Téléversement ... Envoi du fichier %2$s : %1$d%% effectués Téléversement réussi %1$s téléversé @@ -270,7 +268,6 @@ Téléversement annulé Téléchargement annulé Le téléchargement de %1$s a été ajouté à la file d\'attente - Téléchargement ... Téléchargement en cours de %2$s, %1$d%% effectués Téléchargement réussi %1$s téléchargé @@ -344,8 +341,6 @@ Bouton d\'avance rapide Image de la caméra - Demande d\'autorisation… - Tentative d\'identification ... Pas de connexion réseau Connexion sécurisée non disponible Connexion établie @@ -523,8 +518,6 @@ Envoyer l\'Historique Aucune application n\'a été trouvée pour envoyer les journaux. Veuillez installer une application de messagerie ! Journaux de l\'application Android %1$s - Chargement des données… - Tout sélectionner Inversez la sélection @@ -709,7 +702,6 @@ %1$s de données ont été trouvés sur votre stockage externe. Ce contenu sera déplacé vers un emplacement sécurisé sur votre appareil. Les fichiers restants seront supprimés de votre stockage externe après la migration afin d\'éviter des duplications ou failles de sécurité. Votre application a été mise à jour. Cette mise à jour nécessite le déplacement de vos fichiers vers un emplacement plus sécurisé de votre appareil. Ce transfert unique de données est nécessaire et peut prendre une minute ou plus. Commençons - ⚠️ Votre espace libre est actuellement faible sur votre appareil. Certains fichiers pourraient ne pas être transférés, veuillez vérifier que tous les fichiers transférés sont bien présents a la fin du transfert. Migrer maintenant Migration terminée avec succès. Vos fichiers sont maintenant plus sûrs que jamais. Accéder à vos fichiers @@ -730,7 +722,6 @@ Nouveau dans %1$s - Merci d\'utiliser %1$s.\n❤ Exécuter Icône des notes de version Corrections de bugs mineures @@ -739,8 +730,8 @@ Ajout d\'un retour lors de l\'activation/désactivation de la disponibilité hors ligne dans tous les aperçus et mise à jour du menu d\'options selon l\'état du fichier Améliorations de l\'affichage de l\'occupation du stockage Occupation du stockage actualisée fréquemment lors des opérations sur les fichiers et d\'actualisation, et disponible dans la vue de gestion des comptes en plus du menu latéral - Utilisateur léger - Les utilisateurs légers (utilisateurs sans espace personnel) sont désormais pris en charge dans l\'application + Utilisateur Light + Les utilisateurs Light (utilisateurs sans espace personnel) sont désormais pris en charge dans l\'application Correction de la création de fichiers via les fournisseurs d\'applications Les fichiers sont désormais ouverts correctement lorsqu\'ils sont créés à l\'aide de fournisseurs d\'applications Client OpenCloud diff --git a/opencloudApp/src/main/res/values-it/strings.xml b/opencloudApp/src/main/res/values-it/strings.xml new file mode 100644 index 0000000000..2af9076f69 --- /dev/null +++ b/opencloudApp/src/main/res/values-it/strings.xml @@ -0,0 +1,848 @@ + + + + + Nome + Data + Dimensione + Cerca in questa cartella + Cerca spazi + Aggiorna account + Carica + Contenuto da altre app + File + Apri con + Apri con (sola lettura) + Nuova cartella + Impostazioni + Dettagli + Invia + Decrescente + Ordina per + Nuovo documento + Nuovo collegamento + Tutti i file + Disponibili offline + Condivisi tramite link + Impostazioni + Caricamenti + Chiudi + Apri + Caricamento quota + %1$s di %2$s utilizzati (%3$s %%) + Archiviazione piena + Archiviazione quasi piena + Archiviazione quasi del tutto piena + %1$s in uso + Nessuna informazione sull\'utilizzo dell\'archiviazione disponibile + Discussioni GitHub + Aiuto + Generale + Registrazione + Altro + Sicurezza + Novità dell\'ultima versione? + Imposta blocchi per accedere all\'app e consenti tocchi con altre finestre visibili + Tieni traccia dei log prodotti dall\'esecuzione dell\'app + Info app, feedback e altro + Account + Gestisci account + Blocco passcode + Blocco biometrico + Abilita il blocco passcode o schema per attivare questa opzione + Blocca applicazione + Immediatamente + Dopo 1 minuto + Dopo 5 minuti + Dopo 30 minuti + Blocca accesso dal provider documenti + Impedisci ad altre app di accedere ai file degli account dell\'app tramite l\'esploratore file nativo di Android. + Tocchi con altre finestre visibili + Consenti tocchi quando la vista è oscurata da un\'altra finestra visibile. Attiva per utilizzare app con filtro luminosità. + Vuoi davvero abilitare questa funzione? + Usa questa funzione a tuo rischio. Un\'applicazione dannosa potrebbe tentare di indurti a eseguire azioni all\'insaputa, utilizzando altre viste. + Caricamento automatico foto + Gestisci posizione e comportamento delle foto caricate automaticamente + Caricamento automatico video + Gestisci posizione e comportamento dei video caricati automaticamente + Caricamento foto + Carica automaticamente le foto scattate dalla fotocamera + Percorso di caricamento foto + Condizioni per eseguire i caricamenti + I caricamenti verranno attivati quando tutte le opzioni selezionate saranno soddisfatte + Carica foto solo tramite Wi-Fi + Carica foto solo durante la ricarica + Attiva i caricamenti solo tramite Wi-Fi per abilitare questa opzione + Caricamento video + Carica automaticamente i video registrati dalla fotocamera + Percorso di caricamento video + Carica video solo tramite Wi-Fi + Carica video solo durante la ricarica + I caricamenti automatici non sono disponibili per gli utenti light + Avanzate + Gestisci notifiche + Mostra file nascosti + File di log + Cartella dei log vuota! + Abilita la registrazione e i file di log appariranno qui. + Abilita registrazione + Viene utilizzato per registrare i problemi. + Cronologia registrazione + Mostra i log registrati. + Elimina cronologia + Aiuto + Sincronizza contatti, calendari e attività + Installa DAVx⁵ + Accesso al provider documenti + App consigliata per navigare i file sul dispositivo tramite l\'esploratore file nativo di Android + Consiglia a un amico + Feedback + Note legali + Ricorda posizione di condivisione + Ricorda ultima posizione di caricamento condivisione + Blocco schema + Abilita registrazione + Quando attivato, i log potrebbero influire sulle prestazioni e includere informazioni sensibili. Tuttavia, i log non vengono inviati automaticamente ai server OpenCloud. La condivisione dei log con altri è esclusiva responsabilità dell\'utente. + Invia feedback + Versione app + %1$s %2$s versione %3$s (%4$s) + Inserisci il tuo schema + Inserisci il tuo schema + Rimuovi il tuo schema + Schema errato + Reinserisci il tuo schema. + Gli schemi non corrispondono. + Si è verificato un errore durante l\'impostazione del blocco schema. + Si è verificato un errore durante la rimozione del blocco schema. + I blocchi schema e passcode non possono essere attivati contemporaneamente. Disabilita prima lo schema. + I blocchi schema e passcode non possono essere attivati contemporaneamente. Disabilita prima il passcode. + Lo schema non verrà più richiesto. + Lo schema verrà richiesto ogni volta che l\'app viene avviata. + Bloccato + È necessaria un\'azione valida nell\'intent passato a + + + File + Personale + Caricamenti + Offline + Link + Condivisioni + + Prova %1$s sul tuo smartphone! + Voglio invitarti a usare %1$s sul tuo smartphone!\nScarica qui: %2$s + + perché + Autenticazione fallita. Accedi di nuovo per riottenere l\'accesso. + La versione del server è inferiore alla 10 e non è supportata. Aggiorna il server a una versione più recente. + Cambia account + Il token di accesso è scaduto o non è più valido. Accedi di nuovo per riottenere l\'accesso. + Accedi + Verifica server + Indirizzo server https://… + Nome utente + Password + Nuovo su %1$s? + File + Accedi + Carica + Scegli cartella di caricamento + Scegli spazio di caricamento + Nessun account trovato + Non ci sono account %1$s sul dispositivo. Configura prima un account. + Configura + Esci + Nessun file da caricare + %1$s non può caricare un testo come file. + I dati ricevuti non includono alcun file valido. + Impossibile caricare il file + A %1$s non è consentito leggere un file ricevuto. + Il file da caricare non è stato trovato nella sua posizione. Verifica che il file esista. + Si è verificato un errore durante la copia del file in una cartella temporanea. Prova a inviare di nuovo. + secondi fa + Nessun file qui + Nessuno spazio + Spazio: + Nessun file disponibile offline + Nessun link condiviso + Carica contenuti o sincronizza con i tuoi dispositivi! + Spazio personale non disponibile + Nessuna condivisione + Non stai collaborando su risorse di altre persone. + Non hai accesso a nessuno spazio! + I file e le cartelle che contrassegni come disponibili offline appariranno qui. + I file e le cartelle che condividi tramite link appariranno qui. + Caricamento… + Nessuna app trovata per questo tipo di file + Nessuna app trovata per questa azione + Non ci sono file in questa cartella. + Non ci sono cartelle in questa cartella. + Nessun risultato per questa ricerca + Nessun caricamento disponibile. + Carica qualcosa e apparirà qui. + cartella + cartelle + file + file + Tocca un file per visualizzare informazioni aggiuntive. + Carica su %1$s + Scegli un tipo di documento da creare: + File + Dimensione: + Tipo: + Creato: + Ultima sincronizzazione: + Percorso: + Modificato: + Scarica + Immagine file + Pulsante annulla download + Sincronizza + Il file è stato rinominato in %1$s durante il caricamento + Layout elenco + Condividi + Preparazione account per il primo avvio + + No + Rimuovi caricamento + Riprova caricamento + Annulla sincronizzazione + Indietro + Salva ed esci + Errore + Caricamento… + sconosciuto + errore sconosciuto + In attesa + Importante + Cambia password + Rimuovi account + Crea account + Carica da + Nome cartella + Caricamento… + %1$d%% Caricamento %2$s + Caricamento riuscito + %1$s caricato + Caricamento fallito + Il caricamento di %1$s non è stato completato. + Caricamento fallito, devi accedere di nuovo. + Il certificato del server non è attendibile. + Inserisci un nome per il nuovo file. + Il nome del file non può essere vuoto. + Il nome del file non può superare %d caratteri. + Nome file + Caricamento file dalla fotocamera + %d nuove foto verranno caricate. + %d nuovi video verranno caricati. + Caricamenti dalla fotocamera falliti. + Il percorso di origine del caricamento foto non è più valido. + Il percorso di origine del caricamento video non è più valido. + Caricamento file disponibili offline + Caricamento file richiesti tramite Wi-Fi + Il certificato del server non è attendibile. + Caricamenti + Correnti + Falliti (tocca per riprovare) + Caricati + In coda + %d file + %d file + Completato + Annullato + In pausa + Errore di connessione + Il caricamento verrà riprovato a breve + Errore credenziali + Errore cartella + Errore file + File locale non trovato + Errore di autorizzazione + Vietato a causa di una regola del firewall + Conflitto + L\'app è stata terminata + Errore sconosciuto + In attesa di connessione Wi-Fi + In coda + In attesa di caricamento + Tipo di file multimediale non supportato + Il caricamento di %1$s è stato messo in coda + Caricamento annullato + Download annullato + Il download di %1$s è stato messo in coda + Download… + %1$d%% Download di %2$s + Download riuscito + %1$s scaricato + Download fallito + Impossibile visualizzare il file + Il download di %1$s non è stato completato + Non ancora scaricato + Download fallito, devi accedere di nuovo. + per scaricare questo file + Scegli account + Sincronizzazione fallita + Sincronizzazione fallita, devi accedere di nuovo. + La sincronizzazione di %1$s non è stata completata. + Password non valida per %1$s + Conflitti trovati + %1$d file mantenuti sincronizzati non sono stati sincronizzati + File mantenuti sincronizzati falliti + I contenuti di %1$d file non sono stati sincronizzati (%2$d conflitti) + La cartella %1$s non esiste più. + + Inserisci il tuo passcode. + Inserisci il tuo passcode. + Inserisci un nuovo passcode di %1$d cifre. + Il passcode verrà richiesto ogni volta che l\'app viene avviata. + Reinserisci il tuo passcode. + Rimuovi il tuo passcode. + I passcode non corrispondono. + Passcode errato + Si è verificato un errore durante l\'impostazione del blocco passcode. + Si è verificato un errore durante la rimozione del blocco passcode. + Pulsante backspace + Pulsante biometrico + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + + Riprova tra %1$s + + Registra almeno un dato biometrico per utilizzare questa funzione. + Accesso biometrico + Accedi utilizzando la tua credenziale biometrica. + Vuoi attivare anche la sicurezza biometrica? + Sblocco biometrico non disponibile perché il sistema non può fornirlo + + %1$s lettore musicale + %1$s (in riproduzione) + %1$s (caricamento) + Riproduzione di %1$s terminata + Nessun file multimediale trovato + Nessun account fornito + File non in un account valido + Codec multimediale non supportato + Impossibile leggere il file multimediale + File multimediale non codificato correttamente + Timeout durante il tentativo di riproduzione + Impossibile riprodurre in streaming il file multimediale + Impossibile riprodurre il file multimediale con il lettore predefinito + Errore di sicurezza durante la riproduzione di %1$s + Errore di input durante la riproduzione di %1$s + Errore imprevisto durante la riproduzione di %1$s + Pulsante riavvolgimento + Pulsante riproduzione o pausa + Pulsante avanzamento rapido + Foto dalla fotocamera + + Ottenimento autorizzazione… + Tentativo di accesso… + Nessuna connessione di rete + Connessione sicura non disponibile. + Connessione stabilita + Verifica connessione + Configurazione server non valida + Un account per lo stesso utente e server esiste già sul dispositivo. + L\'utente inserito non corrisponde all\'utente di questo account. + Errore sconosciuto! + Istanza server non trovata + Formato dell\'indirizzo server errato + Inizializzazione SSL fallita + Versione server non riconosciuta + Impossibile stabilire la connessione + Connessione sicura stabilita + Nome utente o password errati + Autorizzazione non riuscita + Accesso negato dal server di autorizzazione + Stato imprevisto; inserisci di nuovo l\'indirizzo del server + Autorizzazione scaduta. Autorizza di nuovo + Inserisci la password corrente + Connessione al server di autenticazione… + Il server non supporta questo metodo di autenticazione + Il server non restituisce un ID utente corretto. Contatta un amministratore. + Impossibile autenticarsi a questo server + L\'account non esiste ancora sul dispositivo + + Imposta come disponibile offline + Rimuovi da disponibile offline + Il file è stato impostato come disponibile offline correttamente + Il file non è più impostato come disponibile offline + Rinomina + Rimuovi + Vuoi davvero rimuovere %1$s? + Vuoi davvero rimuovere %1$s e il suo contenuto? + Solo locale + Vuoi davvero rimuovere l\'account %1$s? + Questo account ha caricamenti dalla fotocamera associati; rimuovendo l\'account i caricamenti verranno disattivati. Vuoi davvero rimuovere l\'account %1$s? + Rimuovi link + Rimuovi condivisione privata + Vuoi davvero rimuovere la condivisione di %1$s? + Rimozione riuscita + Rimozione fallita + Inserisci un nuovo nome + Una cartella che contiene questo file è disponibile offline. + Impossibile rinominare la copia locale; prova un nome diverso + Rinomina non completata + Il file esiste già + La cartella esiste già + Un file con nome %1$s esiste già. + Una cartella con nome %1$s esiste già. + Impossibile verificare il file remoto + Contenuti del file già sincronizzati + File non trovato + Trovata una nuova versione sul server. Download… + Download in coda + Caricamento in coda + Impossibile creare la cartella + Impossibile creare il file + Caratteri non consentiti: / \\ + Il nome del file contiene almeno un carattere non valido + Il nome del file non può essere vuoto + Il nome del file non può essere così lungo + Attendi un momento + Verifica credenziali memorizzate + Problema imprevisto; seleziona il file da un\'altra app + Nessun file selezionato + Invia link a + Copia del file dall\'archivio privato + + Accedi con OAuth2 + Connessione al server OAuth2… + + La connessione non è sicura, il traffico HTTP non è consentito. + L\'identità del server non può essere verificata. + - Il certificato del server non è attendibile. + - Il certificato del server è scaduto. + - Le date di validità del certificato del server sono future. + - L\'URL non corrisponde al nome host nel certificato + Vuoi comunque considerare attendibile questo certificato? + Impossibile salvare il certificato. + Dettagli + Nascondi + Rilasciato a: + Rilasciato da: + Nome comune: + Organizzazione: + Unità organizzativa: + Paese: + Stato: + Località: + Validità: + Da: + A: + Firma: + Algoritmo: + Questo algoritmo di digest non è disponibile sul telefono. + Impronta digitale: + Si è verificato un problema nel caricamento del certificato. + Impossibile visualizzare il certificato. + - Nessuna informazione sull\'errore + + Questo è un segnaposto. + placeholder.txt + Immagine PNG + 389 KB + 18/05/2012 12:23 + 12:23:45 + + Conferma + Vuoi davvero disabilitare questa funzione? Le foto in sospeso non verranno caricate. + Vuoi davvero disabilitare questa funzione? I video in sospeso non verranno caricati. + Assicurati che la cartella selezionata sia quella in cui la fotocamera salva le foto scattate. Altrimenti, la funzione non sarà in grado di rilevare le tue foto. Tieni presente che le foto verranno caricate almeno 15 minuti dopo lo scatto. + Assicurati che la cartella selezionata sia quella in cui la fotocamera salva i video registrati. Altrimenti, la funzione non sarà in grado di rilevare i tuoi video. Tieni presente che i video verranno caricati almeno 15 minuti dopo la registrazione. + File in conflitto + C\'è un conflitto per il file %1$s, tocca per risolverlo. + Quali file vuoi mantenere? Se selezioni entrambe le versioni, al file locale verrà aggiunto un numero al nome. + Mantieni entrambi + versione locale + versione server + Si è verificato un errore nella cartella di destinazione. + Sostituisci + + Anteprima immagine + Impossibile visualizzare questa immagine. + + %1$s non può essere copiato nella cartella locale %2$s. + + Impossibile recuperare le condivisioni dal server + Impossibile recuperare utenti e gruppi dal server + Impossibile recuperare le capacità dal server + La condivisione non è abilitata sul server. Contatta l\'amministratore. + Impossibile condividere questo file o cartella + Impossibile annullare la condivisione di questo file o cartella + Impossibile aggiornare questo file o cartella + Inserisci una password + Devi inserire una password. + + Invia + + Copia link + Copiato negli appunti + Nessun testo ricevuto da copiare negli appunti + Errore imprevisto durante la copia negli appunti + Testo copiato da %1$s + + Errore critico: impossibile eseguire operazioni + + Si è verificato un errore durante la connessione al server. + Il server ha impiegato troppo tempo a rispondere. + Il server ha impiegato troppo tempo a connettersi. + Impossibile raggiungere il server. + Si è verificato un errore di rete. + La risorsa è bloccata + + Non hai il permesso %s + + per rinominare questo file + per eliminare questo file + per condividere questo file + per annullare la condivisione di questo file + per aggiornare questa condivisione + per creare il file + per caricare in questa cartella + Caricamento non consentito + Il file non è più disponibile sul server. + + Account + Aggiungi account + Gestisci account + %1$s di %2$s + Utilizzato: + La connessione sicura viene reindirizzata tramite un percorso non sicuro. + + Log + Invia cronologia + Nessuna app per l\'invio dei log trovata. Installa un\'app di posta. + %1$s log app Android + Caricamento dati… + + Seleziona tutto + Selezione inversa + + Sposta + Niente qui. Puoi aggiungere una cartella! + Scegli + Sposta qui + Copia qui + Non hai il permesso di aggiungere contenuti qui! + + Impossibile spostare. Verifica che il file esista. + Non è possibile spostare una cartella al suo interno. + Il file esiste già nella cartella di destinazione. + Si è verificato un errore durante lo spostamento del file o della cartella. + per spostare questo file + + Impossibile copiare. Verifica che il file esista. + Non è possibile copiare una cartella al suo interno. + Il file esiste già nella cartella di destinazione. + Si è verificato un errore durante la copia del file o della cartella. + per copiare questo file + + Caricamenti fotocamera + + La sincronizzazione della cartella %1$s non è stata completata + + condiviso + con te + + %1$s ha condiviso \"%2$s\" con te + \"%1$s\" è stato condiviso con te + + Aggiorna connessione + Indirizzo server + Memoria insufficiente + + Nome utente + + 1 cartella + %1$d cartelle + 1 file + 1 file, 1 cartella + 1 file, %1$d cartelle + %1$d file + %1$d file, 1 cartella + %1$d file, %2$d cartelle + Account per caricare foto + Account per caricare video + Cartella fotocamera (%1$s) + richiesto + Il file originale verrà + Il file originale verrà + Ultima sincronizzazione + Puoi aggiornare le tue preferenze in Impostazioni + Copia file + Sposta file + + mantenuto nella cartella originale + rimosso dalla cartella originale + + Condividi + Condividi %1$s + Utenti e gruppi + Nessun dato ancora condiviso con utenti + Per condividere con altri utenti, il server deve essere almeno versione 8.2 + Aggiungi utente o gruppo + Link pubblici + Crea condivisione tramite link + Modifica condivisione tramite link + Nome link + Annulla + Salva + Scadenza + Password + Protetto + Genera + Copia + Scarica / Visualizza + Scarica/Visualizza/Carica + Solo caricamento (File Drop) + Ottieni link + Condividi con + Condividi con %1$s + + Cerca + + Cerca utenti e gruppi + %1$s (gruppo) + %1$s (remoto) + %1$s ( su %2$s ) + + La versione del server non consente la condivisione con utenti tramite app.\nContatta l\'amministratore + può condividere + può modificare + creare + modificare + eliminare + Interrompi condivisione + fatto + + Riprova + Pulisci + + Il file non è stato trovato nel file system locale + Vuoi davvero rimuovere questi %s elementi? + Vuoi davvero rimuovere gli elementi selezionati e il loro contenuto? + Spazio insufficiente nell\'account + Autenticazione vietata + + Informativa sulla privacy + Informativa sulla privacy + Si è verificato un errore: + Server non disponibile. + + Impossibile avviare lo streaming perché il certificato del server non è attendibile. Il download del file partirà automaticamente. + Questo formato video non è supportato. + Questo file video non è più disponibile sul server. + Lo streaming non è consentito tramite reindirizzamenti incrociati. + Impossibile riprodurre questo video. + + L\'anteprima video non è supportata nella versione di Android in uso. Il download del file partirà automaticamente. + \"Posizione non disponibile\" + Il dispositivo non è connesso a una rete. + Nessun link pubblico ancora creato. + Questa condivisione non ha link. + Chiunque abbia il link può accedere al file/cartella. + ***** + Password * + Scadenza * + Il link pubblico scadrà non oltre %1$d giorni dopo la creazione. + Link %1$s + La password è vuota. + Link privato: solo le persone che hanno accesso al file/cartella possono utilizzarlo. Usalo come link permanente per te stesso o per indirizzare altri a file all\'interno di condivisioni. + Link privato non disponibile per questo file. + Download + Visualizza il nome e l\'avanzamento del download. + Caricamenti + Visualizza il nome e l\'avanzamento del caricamento. + Lettore musicale + Visualizza il lettore musicale + Sincronizzazione file + Visualizza il risultato della sincronizzazione file + Conflitti file + Visualizza i conflitti dei file quando si verificano. + Sei a %1$d tocchi dall\'abilitare i menu sviluppatore + + Valuta l\'app %1$s! + Se ti piace usare questa app, potresti dedicare un momento per valutarla? Il tuo feedback è molto importante per noi. + NO, GRAZIE + PIÙ TARDI + VALUTA ORA + + Salta + Gestisci tutti i tuoi file sincronizzati + Puoi copiare, spostare, eliminare + Condividi file e cartelle + Puoi condividere privatamente o pubblicamente + Multi account + Connettiti a tutti i tuoi server OpenCloud + Caricamenti fotocamera + Le tue foto/video caricati automaticamente + Streaming video + Riproduci i tuoi video senza scaricarli + Nessun browser installato. Installa un browser per consentire un accesso sicuro. + non trovato + + Impossibile sapere se OAuth2 è supportato. + Impossibile conoscere l\'URL di base del server. + Registra richieste e risposte HTTP + I log possono contenere informazioni sensibili. La condivisione dei log con altri è esclusiva responsabilità dell\'utente + + Sincronizzazione account + Impossibile aggiornare gli spazi + + + Trovati %1$s di dati nell\'archiviazione esterna. Verranno spostati nell\'archiviazione sicura del dispositivo. I file rimanenti verranno puliti dall\'archiviazione esterna dopo la migrazione per evitare duplicati e vulnerabilità. + L\'app è stata aggiornata. Questo aggiornamento sposta i tuoi file in una posizione più sicura sul dispositivo. Questa migrazione dati una tantum è necessaria e può richiedere un minuto o più. + Iniziamo + ⚠️ Lo spazio libero sul dispositivo è limitato. Alcuni file potrebbero non essere migrati; verifica che tutti i file scaricati rimangano scaricati dopo il completamento del processo. + Migra ora + Migrazione completata con successo. I tuoi file sono ora più sicuri che mai. + Accedi ai tuoi file + Migrazione dei file in corso. Non spegnere il dispositivo. + In elaborazione… Attendere + Più sicurezza per i tuoi file + + + Scegli un\'opzione per bloccare l\'app: + Blocco passcode + Blocco schema + + Carica contenuti o sincronizza con i tuoi dispositivi! + + + Continua + Nome utente vuoto + + + Novità di %1$s + Grazie per aver scelto %1$s.\n♥️ + Continua + Icona nota di rilascio + Correzioni minori + Alcuni bug sono stati corretti per migliorare l\'esperienza nell\'app + Feedback durante l\'(im)postazione di disp. offline in tutte le anteprime + Aggiunto feedback durante l\'(im)postazione di disp. offline in tutte le anteprime e aggiornato il menu delle opzioni in base allo stato del file + Miglioramenti nella visualizzazione dell\'occupazione dello spazio + L\'occupazione dello spazio viene aggiornata frequentemente nelle operazioni di file e aggiornamento ed è disponibile nella gestione account, oltre che nel menu laterale + Utenti Light + Gli utenti Light (utenti senza spazio personale) sono ora supportati nell\'app + Correzione per la creazione di file tramite provider di app + I file ora vengono aperti correttamente quando vengono creati utilizzando provider di app + Client OpenCloud + Benvenuto nella versione iniziale + + + + Apri sul web + Apri in %1$s (web) + Impossibile aprire sul web + Non ci sono app che supportano questo tipo di file. + Il file richiesto non è ancora disponibile, riprova più tardi. + + + Avviso account + Rimuovi l\'account e accedi di nuovo per ottenere la funzionalità degli spazi. + Non mostrare più + Ho capito + + Testo + + Applica a tutti i %1$s conflitti + File di log scaricato + Vai ai Download + Il file di log %1$s è stato scaricato correttamente nella cartella Download. + + + Formato link non valido + L\'utente non ha accesso al file + Apertura del file dal link + + Aggiorna + Sincronizzazione già in corso + + Almeno %1$d caratteri + Al massimo %1$d caratteri + Almeno %1$d numeri + Almeno %1$d caratteri minuscoli + Almeno %1$d caratteri maiuscoli + Inserisci una password che soddisfi i seguenti criteri: \n + Almeno %1$d dei caratteri speciali: \"%2$s\" + La password scelta è purtroppo molto comune. Scegli una password più difficile da indovinare per la tua sicurezza. + + Mai + 1 ora + 12 ore + 1 giorno + 30 giorni + Elimina copie locali + Rimuovi automaticamente i file scaricati che non sono disponibili offline, quando il tempo dall\'ultimo utilizzo supera il periodo selezionato.\nPeriodo selezionato: %1$s + + URL HTTP non sicuro + L\'URL fornito utilizza HTTP anziché il protocollo crittografato HTTPS. Se continui, la comunicazione non sarà crittografata. + Continua + + Tutti i file scaricati che non sono disponibili offline verranno eliminati per l\'account %1$s + Rimuovi archiviazione locale + Pulisci dati + + Menu + Gestisci account + Cerca + Tipo di vista + Anteprima immagine + Aggiungi link pubblico + Ottieni link privato + Ottieni link pubblico + Elimina link pubblico + Modifica link pubblico + Rimuovi account + Aggiungi nuovo contenuto + Aggiungi nuovo contenuto espanso + Pulisci archiviazione account + Logo + Aggiungi condivisione + Modifica condivisione + Elimina condivisione + %1$s operazioni + Ordina per %1$s crescente + Ordina per %1$s decrescente + Crea nuova cartella + + Crea un collegamento + URL + L\'URL deve iniziare con https:// o http:// per poter essere aperto nel browser + L\'URL non può contenere spazi + .url + Questo collegamento punta a: + Apri link + + repository GitHub]]> + + Link + Pulsante + + Selettore cartelle + Passcode + Schema + Ricevi file esterni + Anteprima video + Note di rilascio + Anteprima immagine + Accesso + Novità + Anteprima audio + Dettagli + Anteprima testo + Aggiunte etichette di testo nella barra inferiore + Sono state aggiunte etichette di testo e viene utilizzato l\'indicatore attivo predefinito per mostrare quale sezione è selezionata nella barra inferiore + + diff --git a/opencloudApp/src/main/res/values-nl/strings.xml b/opencloudApp/src/main/res/values-nl/strings.xml index e270c15107..3f5319e005 100644 --- a/opencloudApp/src/main/res/values-nl/strings.xml +++ b/opencloudApp/src/main/res/values-nl/strings.xml @@ -42,21 +42,21 @@ Meer Beveiliging Wat is er nieuw in de laatste versie\? - Stel vergrendelingen in om toegang tot de app te beheren en toestaan van aanrakingen met andere zichtbare vensters - Houd logboeken bij die zijn geproduceerd door de uitvoering van de app + Beheer van toegangsvergrendeling tot de app en het toestaan van aanrakingen bij andere zichtbare vensters + Logboeken bijhouden van de uitvoering van de app App-info, feedback en overige Accounts Accounts beheren - Toegangscode-vergrendeling - Biometrische vergrendeling - Schakel toegangscode of patroonvergrendeling in om deze optie in te schakelen - Toepassing vergrendelen + Toegangscodeontgrendeling + Biometrische ontgrendeling + Deze optie vereist ontgrendeling met toegangscode of patroon + App vergrendelen Onmiddellijk Na 1 minuut Na 5 minuten Na 30 minuten - Toegang vergrendelen vanaf documentprovider - Vergrendel de toegang van andere apps tot de bestanden van de accounts in de app via de Android bestandsverkenner. + Toegang blokkeren vanuit bestandsbeheer + Blokkeer de toegang van andere apps tot de bestanden van accounts in de app via Android bestandsbeheer. Raakt andere zichtbare vensters Sta aanrakingen toe als de weergave door een ander zichtbaar venster wordt bedekt. Schakel dit in om apps voor lichtfiltering te kunnen gebruiken. Weet u zeker dat u deze functie wilt inschakelen\? @@ -84,7 +84,7 @@ Verborgen bestanden weergeven Logboekbestanden Logboekbestanden wissen! - Logging inschakelen en logbestanden verschijnen hier. + Logboeken inschakelen en logboekbestanden verschijnen hier. Logboeken inschakelen Dit wordt gebruikt om problemen te registeren. Logboekgeschiedenis @@ -100,7 +100,7 @@ Handelsmerk Onthoud locatie delen Onthoud de locatie van de laatste upload van gedeelde bestanden - Patroonvergrendeling + Patroonontgrendeling Logboeken inschakelen Indien ingeschakeld, kunnen logboeken de prestaties beïnvloeden en gevoelige informatie bevatten. De logboeken worden echter niet automatisch verzonden naar OpenCloud-servers. Het delen van logboeken met anderen is de exclusieve verantwoordelijkheid van de gebruiker. Reacties versturen @@ -112,10 +112,10 @@ Onjuist patroon Voer uw patroon opnieuw in. De patronen zijn niet hetzelfde. - Er is een fout opgetreden bij het instellen van de patroonvergrendeling. - Er is een fout opgetreden bij het verwijderen van de patroonvergrendeling. - Patroon- en toegangscodevergrendelingen kunnen niet tegelijkertijd worden ingeschakeld. Schakel patroon eerst uit. - Patroon- en toegangscodevergrendelingen kunnen niet tegelijkertijd worden ingeschakeld. Schakel eerst de toegangscode uit. + Er is een fout opgetreden bij het instellen van de patroonontgrendeling. + Er is een fout opgetreden bij het verwijderen van de patroonontgrendeling. + Ontgrendeling met toegangscode kan niet samen met patroon. Schakel patroon eerst uit. + Ontgrendeling met patroon kan niet samen met toegangscode. Schakel toegangscode eerst uit. Het patroon zal niet meer worden opgevraagd. De patroon wordt elke keer dat de app wordt gestart, opgevraagd. Vergrendeld @@ -139,7 +139,7 @@ Het toegangstoken is verlopen of ongeldig geworden. Meld opnieuw aan om weer toegang te krijgen. Inloggen Server controleren - Server-adres https://&#8230; + Server-adres https://… Gebruikersnaam Wachtwoord Nieuw bij %1$s\? @@ -172,7 +172,7 @@ U hebt geen toegang tot een ruimte! Bestanden en mappen die u offline markeert, worden hier weergegeven. Bestanden en mappen die u per link deelt, worden hier weergegeven. - Laden&#8230; + Laden… Geen app voor dit type bestand Geen app gevonden voor deze actie Er zijn geen bestanden in deze map. @@ -210,7 +210,7 @@ Terug Opslaan & afsluiten Fout - Laden&#8230; + Laden… onbekend onbekende fout In behandeling @@ -220,7 +220,7 @@ Account aanmaken Uploaden van Mapnaam - Uploaden&#8230; + Uploaden… %1$d%% Uploaden %2$s Upload succesvol %1$s geüpload @@ -270,7 +270,7 @@ Upload geannuleerd Download geannuleerd Download van %1$s is in de wachtrij geplaatst. - Downloaden&#8230; + Downloaden… %1$d%% Downloaden %2$s Download succesvol %1$s gedownload @@ -299,8 +299,8 @@ Verwijder uw toegangscode. De toegangscodes zijn niet hetzelfde. Onjuiste toegangscode - Er is een fout opgetreden bij het instellen van de toegangscodevergrendeling. - Er is een fout opgetreden bij het verwijderen van de toegangscodevergrendeling. + Er is een fout opgetreden bij het instellen van de toegangscodeontgrendeling. + Er is een fout opgetreden bij het verwijderen van de toegangscodeontgrendeling. Knop Backspace Knop Biometrie @@ -321,7 +321,7 @@ Biometrische aanmelding Log in met uw biometrische referentie. Wilt u aanvullende biometrische beveiliging activeren\? - Biometrische ontgrendeling is niet beschikbaar op dit systeem + Biometrische ontgrendeling is niet beschikbaar op dit apparaat %1$s muziekspeler %1$s (afspelen) @@ -344,8 +344,8 @@ Knop Vooruit Foto van camera - Autorisatie krijgen&#8230; - Proberen in te loggen&#8230; + Autorisatie verkrijgen… + Aanmelden… Geen netwerkverbinding Beveiligde verbinding niet beschikbaar. Verbinding tot stand gebracht @@ -523,7 +523,7 @@ Geschiedenis versturen Geen app voor het verzenden van gevonden logboeken. Installeer een mail-app. %1$s Android app-logboeken - Gegevens laden&#8230; + Gegevens laden… Alles selecteren Selectie omkeren @@ -708,7 +708,7 @@ %1$s gegevens gevonden over uw externe opslag. Het wordt verplaatst naar de veilige opslag op uw apparaat. De overige bestanden worden na de migratie uit jouw externe opslag verwijderd om duplicaten en kwetsbaarheid te voorkomen. De app is bijgewerkt. Deze app-update migreert de bestanden naar een veiligere locatie op dit apparaat. Deze eenmalige datamigratie is vereist en kan tot een minuut of langer duren. Laten we beginnen - &#9888;️ De vrije ruimte is momenteel beperkt op dit apparaat. Sommige bestanden zijn mogelijk niet gemigreerd. Controleer of alle gedownloade bestanden gedownload blijven nadat het proces is voltooid. + ⚠️ Uw vrije ruimte is momenteel beperkt op uw apparaat. Sommige bestanden zijn mogelijk niet gemigreerd. Controleer of alle gedownloade bestanden gedownload blijven nadat het proces is voltooid. Migreer nu Migratie succesvol afgerond. Uw bestanden zijn nu veiliger dan ooit. Toegang tot uw bestanden @@ -729,7 +729,7 @@ Nieuw in %1$s - Bedankt voor het gebruik van %1$s.\n&#10084; + Bedankt voor het gebruik van %1$s.\n♥️ Doorgaan Pictogram Release note Kleine bugfixes diff --git a/opencloudApp/src/main/res/values-pl/strings.xml b/opencloudApp/src/main/res/values-pl/strings.xml index e4b2f90e20..16f1b63812 100644 --- a/opencloudApp/src/main/res/values-pl/strings.xml +++ b/opencloudApp/src/main/res/values-pl/strings.xml @@ -9,6 +9,7 @@ Szukaj w przestrzeniach Odśwież konto Prześlij + Prześlij z innych aplikacji Pliki Otwórz za pomocą Otwórz za pomocą (tylko do odczytu) @@ -21,11 +22,19 @@ Nowy dokument Nowy skrót Wszystkie pliki + Dostępne offline + Udostępnione przez link Ustawienia Przesłane pliki Zamknij Otwórz + Sprawdzanie przydziału... + %1$s użyte z %2$s + Przekroczono przydział + Zbliżasz się do limitu przydziału + Krytyczny stan przydziału %1$s w użyciu + Użyte miejsce niedostępne Dyskusje na GitHubie Pomoc Ogólne @@ -33,46 +42,151 @@ Więcej Bezpieczeństwo Co nowego w najnowszej wersji\? + Zarządzaj opcjami bezpieczeństwa + Ustawienia logowania + Dodatkowe ustawienia Konta Zarządzanie kontami + Kod blokady + Uwierzytelnianie biometryczne + Użyj biometrii do odblokowania aplikacji Zablokuj aplikację + Natychmiast Po 1 minucie Po 5 minutach Po 30 minutach + Zablokuj dostęp dostawcy dokumentów + Wymagaj kodu PIN + Zezwalaj na dotyk przy innych widocznych oknach + Zezwalaj na interakcję przy nakładających się oknach Czy na pewno chcesz aktywować tę funkcję\? + Czy na pewno chcesz to włączyć? + Przesyłanie zdjęć + Konfiguruj ustawienia przesyłania zdjęć + Przesyłanie wideo + Konfiguruj ustawienia przesyłania wideo + Automatyczne przesyłanie zdjęć + Przesyłaj zdjęcia z aparatu automatycznie + Ścieżka przesyłania zdjęć + Warunki przesyłania + Opcje sieci i zasilania + Przesyłaj tylko przez Wi-Fi + Przesyłaj tylko podczas ładowania + Przesyłaj pliki tylko podczas ładowania + Automatyczne przesyłanie wideo + Przesyłaj wideo z aparatu automatycznie + Ścieżka przesyłania wideo + Przesyłaj tylko przez Wi-Fi + Przesyłaj tylko podczas ładowania + Automatyczne przesyłanie niedostępne Zaawansowane + Powiadomienia Pokaż ukryte pliki Pliki logów Folder logów jest pusty! + Brak logów Włącz logowanie + Włącz logowanie diagnostyczne Historia logowania + Historia zapisanych logów Usuń historię Pomoc + Synchronizuj kalendarz i kontakty Zaistaluj DAVx⁵ + Dostawca dokumentów + Dostęp przez dostawcę dokumentów + Poleć znajomemu + Prześlij opinię + Impressum + Pamiętaj ostatnią lokalizację udostępniania + Pamiętaj ostatnią lokalizację + Wzór blokady Włącz logowanie + Rejestruj błędy do pliku Wyślij informację zwrotną + Wersja aplikacji + Informacje o bieżącej wersji + Wprowadź wzór blokady Wprowadź swój wzorzec Usuń swój wzorzec Nieprawidłowy wzorzec + Wprowadź ponownie wzór Wzorce nie są takie same. Wystąpił problem podczas ustawiania wzoru blokady. Wystąpił problem podczas usuwania wzoru blokady. + Wzór już ustawiony + Kod już ustawiony + Wzór nie jest już wymagany + Skonfiguruj wzór blokady Zablokowano + Nieprawidłowy argument + Pliki Osobiste Przesłane pliki + Offline Linki + Udostępnienia + + Polecam OpenCloud + Wypróbuj aplikację OpenCloud + + Błąd: %1$s + Błąd uwierzytelniania + Serwer nie jest obsługiwany + Zaloguj się + Błąd logowania OAuth + Zaloguj się + Sprawdź serwer + URL serwera Nazwa użytkownika Hasło + Zarejestruj się Pliki Zaloguj się Prześlij + Wybierz miejsce docelowe + Wybierz przestrzeń + Brak konta + Zaloguj się, aby przesłać pliki + Skonfiguruj + Wyjdź + Brak pliku do przesłania + Otrzymano tekst zamiast pliku + Nie wybrano pliku + Nie można przesłać pliku + Brak uprawnień do odczytu + Nie odnaleziono pliku źródłowego + Nie można skopiować pliku źródłowego + sekundy temu + Brak plików + Brak przestrzeni + Przestrzeń + Brak plików offline + Brak udostępnionych linków + Prześlij pliki lub utwórz nowe foldery + Lekki użytkownik Brak udziałów + Brak udostępnionych elementów + Nie masz jeszcze żadnych przestrzeni + Pliki offline pojawią się tutaj + Udostępnione linki pojawią się tutaj + Ładowanie… + Brak aplikacji do otwarcia tego pliku + Brak aplikacji do wykonania tej akcji + Brak plików + Tylko foldery + Brak wyników wyszukiwania + Brak przesłanych plików + Wysłane pliki pojawią się tutaj folder foldery plik pliki + Wybierz plik + Prześlij do + Wybierz typ dokumentu do utworzenia: Pliki Rozmiar: Typ: @@ -81,32 +195,164 @@ Ścieżka: Zmodyfikowano: Pobierz + Podgląd obrazu + Anuluj + Synchronizuj plik + Zmieniono nazwę podczas przesyłania + Układ listy + Udostępnij + Przygotowywanie konta Tak Nie + Usuń z listy + Ponów + Anuluj synchronizację Cofnij Zapisz i zamknij Błąd + Ładowanie… nieznany + Nieznany błąd + Oczekujące + Ważne Zmień hasło + Usuń konto + Utwórz konto + Wybierz miejsce do przesłania Nazwa folderu + Przesyłanie w toku + Trwa przesyłanie plików... + Przesyłanie zakończone + Plik przesłany pomyślnie + Błąd przesyłania + Przesyłanie nie powiodło się + Błąd uwierzytelniania + Niezaufany certyfikat SSL + Prześlij tekst + Nazwa pliku nie może być pusta + Nazwa pliku jest zbyt długa Nazwa pliku + Przesyłanie z aparatu + Przesyłanie zdjęć + Przesyłanie wideo + Błąd ścieżki aparatu + Błąd przesyłania zdjęć + Błąd przesyłania wideo + Przesyłanie plików offline + Przesyłanie przez Wi-Fi + Niezaufany certyfikat Przesłane pliki + Bieżące + Nieudane + Zakończone + W kolejce %d plików + 1 plik + Zakończono + Anulowano + Wstrzymano + Błąd połączenia + Nie powiodło się, ponowienie + Błąd logowania + Błąd folderu + Błąd pliku + Błąd lokalnego pliku + Błąd uprawnień + Błąd zapory sieciowej + Konflikt plików + Usługa przerwana Nieznany błąd + Oczekiwanie na Wi-Fi + W kolejce + Oczekiwanie na przesłanie + Nieobsługiwany typ multimediów + Dodano do kolejki + Przesyłanie anulowane + Pobieranie anulowane + Pobieranie dodane do kolejki + Pobieranie w toku + Trwa pobieranie... + Pobieranie zakończone + Pobrano pomyślnie Błąd pobierania + Błąd podglądu + Pobieranie nie powiodło się + Jeszcze nie pobrano + Błąd uwierzytelniania + Brak uprawnień do pobierania + Wybierz konto + Błąd synchronizacji + Błąd autoryzacji + Synchronizacja nie powiodła się + Synchronizacja wymaga logowania + Konflikty w ulubionych + Znaleziono konflikty + Błąd w ulubionych + Nie można zsynchronizować ulubionych + Folder został usunięty + + Wprowadź kod + Skonfiguruj kod blokady + Zmień na nowy kod + Ustaw kod dostępu + Wprowadź ponownie kod + Usuń kod blokady + Kody nie pasują + Błędny kod + Błąd ustawiania kodu + Błąd usuwania kodu + Cofnij + Biometria + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + + Spróbuj ponownie później + + Brak zapisanych danych biometrycznych + Autoryzacja + Potwierdź tożsamość + Biometria + Biometria niedostępna + %1$sodtwarzacz muzyki %1$sodtwarzanie %1$sładowanie %1$sodtwarzanie zakończone + Brak plików do odtworzenia + Brak konta + Plik nie znajduje się w chmurze + Format nieobsługiwany + Błąd wejścia/wyjścia + Uszkodzony plik Przekroczenie czasu przy próbie odtwarzania Plik nie może być streamowany + Nieznany błąd + Błąd bezpieczeństwa + Błąd operacji I/O + Nieoczekiwany błąd + Przewiń do tyłu Przycisk odtwórz lub zapauzuj Przycisk szybkiego przewijania Obraz z kamery + Autoryzowanie... + Logowanie... Brak połączenia z siecią + Bezpieczne połączenie niemożliwe Połączenie nawiązane Testowanie połączenia + Brak konfiguracji + To konto już istnieje + Konta się nie zgadzają Wystąpił nieznany błąd! Nie odnaleziono instancji serwera Nieprawidłowy format adresu serwera @@ -115,47 +361,125 @@ Nie można nawiązać połączenia Bezpieczne połączenie nawiązane Zła nazwa użytkownika lub hasło + Błąd OAuth + Odmowa dostępu + Wprowadź ponownie adres URL + Sesja wygasła Wprowadź hasło + Łączenie z serwerem logowania... + Nieobsługiwana metoda logowania + Nie można pobrać nazwy użytkownika + Odmowa dostępu do serwera + Konto nie istnieje + + Ustaw jako dostępne offline + Usuń z dostępnych offline + Plik będzie dostępny bez sieci + Plik nie będzie już dostępny offline Zmień nazwę Usuń Czy na pewno chcesz usunąć %1$s\? Czy na pewno chcesz usunąć %1$s i jego zawartość\? + Usuń lokalnie + Czy na pewno chcesz usunąć konto? + Automatyczne przesyłanie zostanie zatrzymane. Usuń link + Usuń prywatne udostępnienie + Czy na pewno chcesz usunąć udostępnienie? + Usunięto pomyślnie + Błąd usuwania Wprowadź nową nazwę + Dostępne offline (dziedziczone) + Błąd zmiany nazwy (lokalnie) + Błąd zmiany nazwy (na serwerze) Plik już istnieje Folder już istnieje + Taki plik już istnieje + Taki folder już istnieje + Błąd synchronizacji pliku + Zsynchronizowano Plik nie został znaleziony + Znaleziono nową wersję na serwerze + Dodano do pobierania Dodano do kolejki przesyłania Nie udało się utworzyć folderu Nie udało się utworzyć pliku + Niedozwolone znaki w nazwie + Znaki zablokowane przez serwer Nazwa pliku nie może być pusta + Nazwa zbyt długa Proszę czekać + Sprawdzanie danych logowania... + Błąd pobierania pliku + Nie wybrano pliku Wyślij link do + Kopiowanie... + Logowanie za pomocą oAuth2 + Połączenie logowania OAuth + + Niezabezpieczone połączenie SSL + Walidator SSL + Certyfikat nie jest zaufany Certyfikat serwera wygasł + Certyfikat jeszcze nie jest ważny + Nazwa hosta niezgodna z certyfikatem + Czy ufasz temu certyfikatowi? + Nie zapisano certyfikatu Szczegóły Ukryj + Podmiot: + Wystawca: + Nazwa (CN): Organizacja: Jednostka: Kraj: + Stan/Województwo: Lokalizacja: + Ważność: Od: Do: Podpis: Algorytm: + Algorytm skrótu niedostępny + Odcisk certyfikatu: + Problem z załadowaniem certyfikatu + Pusty certyfikat - Brak informacji o tym błędzie + To jest przykładowy tekst. placeholder.txt Obraz PNG 389 KB 2012/05/18 12:23 + 00:00 + Potwierdź + Wyłączyć przesyłanie zdjęć? + Wyłączyć przesyłanie wideo? + Nieprawidłowy folder dla zdjęć + Nieprawidłowy folder dla wideo + Konflikt + Wystąpił konflikt wersji + Którą wersję chcesz zachować? Zachowaj oba lokalna wersja wersja serwera + Błąd konfliktu Zamień Podgląd obrazu + Nieznany format obrazu + + Nie skopiowano pliku lokalnego + + Błąd pobierania udostępnień + Błąd pobierania odbiorców + Błąd pobierania możliwości serwera + Serwer nie wspiera tego API + Błąd udostępniania linku + Błąd usuwania linku + Błąd aktualizacji linku Wprowadź hasło Musisz wprowadzić hasło. @@ -163,18 +487,76 @@ Kopiuj link Skopiowano do schowka + Brak tekstu do skopiowania + Nieoczekiwany błąd schowka + Schowek + + Błąd usługi w tle + + Błąd połączenia z gniazdem + Przekroczenie czasu połączenia + Przekroczenie czasu połączenia + Host jest niedostępny + Wystąpił błąd sieci + Zasób jest zablokowany + + Brak uprawnień + + Brak uprawnień do zmiany nazwy + Brak uprawnień do usunięcia + Brak uprawnień do udostępniania + Brak uprawnień do cofnięcia udostępniania + Brak uprawnień do edycji + Brak uprawnień do utworzenia + Brak uprawnień do przesyłania + Przesyłanie zablokowane + Brak pliku do pobrania + Konta Dodaj konto Zarządzanie kontami + %1$s z %2$s Wykorzystano: + Niezabezpieczone przekierowanie + Logi + Wyślij logi + Brak aplikacji e-mail + Logi aplikacji OpenCloud + Przygotowywanie... + Zaznacz wszystko + Odwróć zaznaczenie + Przenieś + Brak plików do przeniesienia Wybierz Przenieś tutaj Kopiuj tu + Brak uprawnień do tego folderu + + Nie znaleziono pliku do przeniesienia + Nie można przenieść folderu do jego podfolderu Ten plik już istnieje w folderze docelowym + Błąd podczas przenoszenia + Brak uprawnień do przeniesienia + + Nie znaleziono pliku do skopiowania + Nie można skopiować folderu do jego podfolderu Ten plik już istnieje w folderze docelowym + Błąd podczas kopiowania + Brak uprawnień do skopiowania + + Przesyłanie z aparatu + + Błąd synchronizacji folderu + + udostępniono + udostępniono dla Ciebie + + Użytkownik udostępnił dla Ciebie + Udostępniono Tobie + Odśwież połączenie Adres serwera Brak wystarczającej pamięci @@ -185,26 +567,56 @@ %1$d folderów 1 plik 1 plik, 1 folder + 1 plik, %1$d folderów %1$d plików + %1$d plików, 1 folder + %1$d plików, %2$d folderów + Konto dla zdjęć + Konto dla wideo + Folder źródłowy wymagane + Zachowanie przesyłania + Akcja po przesłaniu Ostatnia synchronizacja + Brak uprawnień do powiadomień Skopiuj plik Przenieś plik + Zachowaj plik oryginalny + Usuń po przesłaniu + + Udostępnij + Udostępnij plik Użytkownicy i Grupy + Nie znaleziono użytkowników + Niekompatybilna wersja Dodaj użytkownika do grupy Linki publiczne + Utwórz link publiczny + Edytuj link Nazwa linku Anuluj Zapisz + Data wygaśnięcia Hasło + Zabezpiecz hasłem Generuj Kopiuj Pobierz / Wyświetl + Pobierz i przesyłaj + Tylko przesyłanie Pobierz link + Udostępnij z + Edytuj udostępnienie + Szukaj + Szukaj... %1$s (grupa) + zdalne udostępnienie + znane + + Odbiorca niedostępny można udostępniać można edytować utwórz @@ -216,61 +628,219 @@ Ponów Wyczyść + Brak pliku lokalnego + Usunąć zaznaczone pliki? + Usunąć zaznaczone foldery? + Brak miejsca na koncie + Odmowa dostępu + Polityka prywatności Polityka prywatności Wystąpił błąd: + Usługa niedostępna + + Błąd certyfikatu + Nieznany format + Nie znaleziono pliku mediów + Zbyt wiele przekierowań + Błąd odtwarzania wideo + + Ten format wideo nie jest obsługiwany \"Pozycja nie jest dostępna\" + Brak sieci + Brak publicznych linków + Brak linku dla tego udostępnienia + Ostrzeżenie o linkach ***** Hasło * + Wygasa: + Wygaśnie automatycznie po %1$slink Hasło jest puste. + Prywatny link skopiowany + Błąd generowania prywatnego linku + Pobieranie plików + Powiadomienia o pobieraniu Przesłane pliki + Powiadomienia o przesyłaniu Odtwarzacz muzyki + Powiadomienia o muzyce + Synchronizacja + Powiadomienia o synchronizacji + Konflikty + Powiadomienia o konfliktach + Jeszcze kilka kliknięć + + Oceń nas + Podoba Ci się nasza aplikacja? NIE, DZIĘKI PÓŹNIEJ + Oceń teraz + + Zaznaczono %d element + Zaznaczono %d elementy + Zaznaczono %d elementów + Zaznaczono %d elementów + + Pomiń + Bezpieczne miejsce na pliki + Synchronizuj i udostępniaj. + Zdjęcia z aparatu + Automatyczny backup. + Dostęp do plików offline + Przeglądaj pliki bez sieci. + Łatwe udostępnianie + Twórz linki publiczne. Streaming video + Oglądaj filmy bezpośrednio z chmury. + Brak zainstalowanej przeglądarki + Nie znaleziono + + Serwer nie wspiera OAuth2 + Błąd podczas pobierania bazowego URL + Logi HTTP + Przechowuj szczegółowe logi + + Synchronizacja konta... + Błąd synchronizacji przestrzeni + + + Migracja pamięci masowej + Wprowadzenie do migracji Zacznijmy + Za mało miejsca na migrację Migruj teraz + Migracja zakończona + Dostęp do plików + Trwa migracja... + Migruj + Migracja pamięci + + + Bezpieczeństwo wymuszone + Opcja bezpieczeństwa 1 + Opcja bezpieczeństwa 2 + + Prześlij zawartość lub zsynchronizuj + + + Kontynuuj + Nazwa użytkownika nie może być pusta + + + Nowości w %1$s + Pozdrawiamy + Kontynuuj + Ikona wydania + Poprawki błędów + Naprawiliśmy znane błędy + Opinie + Poprawa działania podglądu + Przydział + Usprawnienia limitów + Lekcy użytkownicy + Opcje dla darmowych kont + Poprawki + Usprawnienia interfejsu Klient OpenCloud + Pierwsze wydanie + + + + Otwórz w przeglądarce + Otwórz za pomocą przeglądarki + Nie udało się otworzyć w przeglądarce + Opcja nieobsługiwana + Jeszcze niedostępne + + + Ostrzeżenie kont + Proszę potwierdzić ustawienia konta Nie pokazuj ponownie + Rozumiem + Tekst + Zastosuj do wszystkich + Log pobrany Idź do pobranych + Plik logów został zapisany na dysku + + + Nieprawidłowy link + Brak dostępu do linku + Wczytywanie linku... + Odśwież + Trwa synchronizacja + + Zbyt mało znaków Conajmniej %1$d znaków Conajmniej %1$d liczb + Wymagane małe litery + Wymagane duże litery + Zasady tworzenia hasła: + Wymagane znaki specjalne + To hasło znajduje się na czarnej liście + Nigdy 1 godzina 12 godzin 1 dzień 30 dni + Usuwaj kopie lokalne + Po jakim czasie usuwać lokalnie pobrane pliki + Niezabezpieczony URL HTTP + Próbujesz użyć niezabezpieczonego połączenia. Kontynuuj + Czy chcesz usunąć wszystkie dane konta? + Czyszczenie danych konta Usuń dane Menu Zarządzanie kontami Szukaj + Zmień widok + Podgląd obrazu otwarty Dodaj publiczny link + Uzyskaj prywatny link + Uzyskaj publiczny link Usuń publiczny link Edytuj publiczny link + Usuń to konto + Dodaj element + Dodaj element (rozwinięte) + Zwolnij miejsce na koncie Logo Dodaj udział Edycja udziału Usuń udział + Opcje pliku + Sortuj po nazwie (rosnąco) + Sortuj po nazwie (malejąco) Utwórz nowy folder + Utwórz skrót URL + Wprowadź adres URL, do którego ma prowadzić skrót. + Adres URL nie może zawierać spacji .url + Otwórz ten skrót w przeglądarce Otwórz link + Skontaktuj się z nami, aby przekazać swoją opinię. + Link Przycisk + Wybór folderu + Kod blokady Wzorzec + Odbieranie plików zewnętrznych Podgląd video Informacje o wydaniu Podgląd obrazu @@ -279,4 +849,7 @@ Podgląd audio Szczegóły Podgląd tekstu - + Nowy pasek nawigacji + Łatwiejsze poruszanie się po aplikacji + + From f4810c0e32e64c60a2735d6dd751901f8cfc51ae Mon Sep 17 00:00:00 2001 From: Markus Goetz Date: Mon, 15 Jun 2026 14:14:10 +0200 Subject: [PATCH 13/13] ReadRemoteFileOperation: Also read checksums For future use. --- .../android/lib/resources/files/ReadRemoteFileOperation.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/ReadRemoteFileOperation.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/ReadRemoteFileOperation.kt index 0159e1273c..1bbc17e1d8 100644 --- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/ReadRemoteFileOperation.kt +++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/ReadRemoteFileOperation.kt @@ -23,6 +23,7 @@ package eu.opencloud.android.lib.resources.files +import at.bitfire.dav4jvm.PropertyRegistry import eu.opencloud.android.lib.common.OpenCloudClient import eu.opencloud.android.lib.common.accounts.AccountUtils import eu.opencloud.android.lib.common.http.HttpConstants.HTTP_MULTI_STATUS @@ -30,6 +31,8 @@ import eu.opencloud.android.lib.common.http.HttpConstants.HTTP_OK import eu.opencloud.android.lib.common.http.methods.webdav.DavConstants.DEPTH_0 import eu.opencloud.android.lib.common.http.methods.webdav.DavUtils import eu.opencloud.android.lib.common.http.methods.webdav.PropfindMethod +import eu.opencloud.android.lib.common.http.methods.webdav.properties.OCChecksums +import eu.opencloud.android.lib.common.http.methods.webdav.properties.OCShareTypes import eu.opencloud.android.lib.common.network.WebdavUtils import eu.opencloud.android.lib.common.operations.RemoteOperation import eu.opencloud.android.lib.common.operations.RemoteOperationResult @@ -62,6 +65,9 @@ class ReadRemoteFileOperation( if (client.account == null) { throw AccountUtils.AccountNotFoundException() } + PropertyRegistry.register(OCShareTypes.Factory()) + PropertyRegistry.register(OCChecksums.Factory()) + val propFind = PropfindMethod( url = getFinalWebDavUrl(), depth = DEPTH_0,