Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import dev.obiente.nextcloudnative.app.DurableUploadStatus
import dev.obiente.nextcloudnative.app.LocalUploadFile
import dev.obiente.nextcloudnative.app.MAX_DURABLE_UPLOAD_MESSAGE_CHARACTERS
import dev.obiente.nextcloudnative.app.MultipartTextField
import dev.obiente.nextcloudnative.app.NextcloudAccountId
import dev.obiente.nextcloudnative.app.NextcloudAccountRecord
import dev.obiente.nextcloudnative.app.NextcloudApiMethod
import dev.obiente.nextcloudnative.app.NextcloudMultipartUploadRequest
import dev.obiente.nextcloudnative.app.NextcloudSession
Expand Down Expand Up @@ -187,25 +189,13 @@ internal class DeckAttachmentUploadWorker(
picker: AndroidLocalUploadPicker,
jobId: String,
): Result {
val accountServices = AndroidNextcloudServices(applicationContext)
val session = accountServices.loadSession()
if (session == null || NextcloudDocumentIds.accountKey(session) != initial.accountId) {
val retainedSession = resolveStoredAndroidAccountSession(
accountIdentity = initial.accountId,
listAccounts = accountServices::listAccounts,
loadSession = { accountId -> accountServices.loadSession(accountId) },
)
if (durableUploadAccountMismatchOutcome(initial.accountId, retainedSession) ==
DurableUploadAccountMismatchOutcome.DeferRetainedAccount
) {
recordUploadDiagnostic(
severity = SupportDiagnosticSeverity.Warning,
outcome = "account-deferred",
accountId = initial.accountId,
jobId = jobId,
)
return Result.success()
}
val services = AndroidNextcloudServices(applicationContext)
val session = resolveDurableUploadSession(
expectedAccountId = initial.accountId,
accounts = services.listAccounts(),
loadSession = services::loadSession,
Comment thread
veryCrunchy marked this conversation as resolved.
)
if (session == null) {
store.transition(
jobId,
expected = DurableUploadState.Queued,
Expand Down Expand Up @@ -247,9 +237,9 @@ internal class DeckAttachmentUploadWorker(
target = DurableUploadState.Uploading,
message = null,
) ?: return Result.success()
val services = AndroidNextcloudServices(applicationContext, localUploadPicker = picker)
val uploadServices = AndroidNextcloudServices(applicationContext, localUploadPicker = picker)
val outcome = runCatching {
services.executeNextcloudMultipartUpload(session, started.request)
uploadServices.executeNextcloudMultipartUpload(session, started.request)
}
outcome.onSuccess { response ->
val state = durableUploadStateForHttpResponse(response.status)
Expand Down Expand Up @@ -359,6 +349,19 @@ internal fun queuedDurableUploadsForAccount(
job.accountId == accountId && job.state == DurableUploadState.Queued
}

internal fun resolveDurableUploadSession(
expectedAccountId: String,
accounts: List<NextcloudAccountRecord>,
loadSession: (NextcloudAccountId) -> NextcloudSession?,
): NextcloudSession? {
val account = accounts.singleOrNull { record ->
NextcloudDocumentIds.accountKey(record.serverUrl, record.loginName) == expectedAccountId
} ?: return null
return loadSession(account.id)?.takeIf { session ->
NextcloudDocumentIds.accountKey(session) == expectedAccountId
}
}

internal data class AndroidDurableMultipartUploadJob(
val id: String,
val accountId: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import dev.obiente.nextcloudnative.app.DurableUploadState
import dev.obiente.nextcloudnative.app.NextcloudApiMethod
import dev.obiente.nextcloudnative.app.NextcloudMultipartUploadRequest
import dev.obiente.nextcloudnative.app.NextcloudSession
import dev.obiente.nextcloudnative.app.accountRecord
import dev.obiente.nextcloudnative.app.afterProcessRecovery
import dev.obiente.nextcloudnative.app.localUploadFile
import java.io.IOException
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
import org.json.JSONArray

Expand Down Expand Up @@ -355,6 +357,62 @@ class AndroidDurableMultipartUploadPolicyTest {
)
}

@Test
fun `background upload resolves the queued account instead of the active account`() {
val queuedSession = fixtureSession("alice")
val activeSession = fixtureSession("bob")
val loadedAccountIds = mutableListOf<String>()

val resolved = resolveDurableUploadSession(
expectedAccountId = NextcloudDocumentIds.accountKey(queuedSession),
accounts = listOf(activeSession.accountRecord(), queuedSession.accountRecord()),
loadSession = { accountId ->
loadedAccountIds += accountId.storageKey
when (accountId) {
queuedSession.accountId -> queuedSession
activeSession.accountId -> activeSession
else -> null
}
},
)

assertEquals(queuedSession, resolved)
assertEquals(listOf(queuedSession.accountId.storageKey), loadedAccountIds)
}

@Test
fun `background upload never substitutes another account on the same server path`() {
val queuedSession = fixtureSession("alice")
val otherSession = fixtureSession("bob")
var credentialRead = false

val missing = resolveDurableUploadSession(
expectedAccountId = NextcloudDocumentIds.accountKey(queuedSession),
accounts = listOf(otherSession.accountRecord()),
loadSession = {
credentialRead = true
otherSession
},
)

assertNull(missing)
assertFalse(credentialRead)
}

@Test
fun `background upload rejects a credential that does not match its registry owner`() {
val queuedSession = fixtureSession("alice")
val otherSession = fixtureSession("bob")

val resolved = resolveDurableUploadSession(
expectedAccountId = NextcloudDocumentIds.accountKey(queuedSession),
accounts = listOf(queuedSession.accountRecord(), otherSession.accountRecord()),
loadSession = { otherSession },
)

assertNull(resolved)
}

private fun fixtureJob(
index: Int,
account: String,
Expand Down Expand Up @@ -393,6 +451,12 @@ class AndroidDurableMultipartUploadPolicyTest {

private fun selectionId(index: Int): String = "selection-${index.toString().padStart(16, '0')}"

private fun fixtureSession(loginName: String): NextcloudSession = NextcloudSession(
serverUrl = "https://cloud.example.test/nextcloud",
loginName = loginName,
appPassword = "fixture-password",
)

private companion object {
const val ACCOUNT_A = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
const val ACCOUNT_B = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
Expand Down
7 changes: 7 additions & 0 deletions changes/unreleased/172-account-background-uploads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
category: fix
issue: 172
pull: 438
platforms: android
user-facing: yes

Queued Deck attachment uploads now keep using the account that created them after another account is selected.
2 changes: 1 addition & 1 deletion tools/kotlin-file-size-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/NextcloudMediaViewer.kt
ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/NextcloudNativeApp.kt|12435
ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/NextcloudNotes.kt|1693
ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/NextcloudPhotoEditor.kt|808
ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/NextcloudPlatform.kt|1755
ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/NextcloudPlatform.kt|1749
ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/PhotoEditing.kt|847
ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/PhotoFolderBrowsing.kt|895
ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/PhotoTimelinePaging.kt|860
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1378,8 +1378,8 @@ interface NextcloudPlatformServices : NextcloudAccountCredentialServices {
/**
* Streams one picker-authorized file to a reviewed same-origin multipart endpoint.
*
* Implementations attach the active account credentials, reject redirects, enforce both
* request and response limits, and never accept an arbitrary local path from shared code.
* Implementations use the supplied session's credentials, including for retained background
* work, reject redirects and arbitrary local paths, and enforce request and response limits.
*/
suspend fun executeNextcloudMultipartUpload(
session: NextcloudSession,
Expand Down
2 changes: 1 addition & 1 deletion website/public/screenshots/capture-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@
"ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/NextcloudNotesCache.kt": "ac2206703b224364c1a3ff4097026c9c81d856042c20d31e5c28358016c3062d",
"ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/NextcloudPeople.kt": "cff910ea2cc77211ef81779c49ee0c957851f2b4a3ed32b857b12ded1cee643b",
"ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/NextcloudPhotoEditor.kt": "34d9b43cf3bbfc8342958dc40f2df7b4573f30a2ae1bd9bcb8bb470151313a3d",
"ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/NextcloudPlatform.kt": "4b611d0d9f5a7c91f820230ca0e01530972e8cfabb60ce11bd5b4146d4400a20",
"ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/NextcloudPlatform.kt": "311bd57ea50d0e7db1d7e03036aa00f6590bba3a5eeff7f903d17e3303314098",
"ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/NextcloudResponseLimits.kt": "fead6cbf4f723ea46f99c9155b190967205d24f198687b7bfd5a213c474374b8",
"ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/NextcloudSessionLoading.kt": "0abcfe2da22b8e49f6ee292d8b340cc36dafd2cf0658cbb8ca847481a351fe19",
"ui/src/commonMain/kotlin/dev/obiente/nextcloudnative/app/NextcloudStatusMessages.kt": "5154dd95c432c91e306c09372ef7367f6b43be8c9d52826b36ecd8ecbfcbf3dc",
Expand Down