Provide distinct cache directory to the Rust SDK.

This commit is contained in:
Benoit Marty
2024-08-30 18:34:52 +02:00
parent 9f67c65c2c
commit 93cace6954
15 changed files with 153 additions and 39 deletions

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.migration.impl.migrations
import com.squareup.anvil.annotations.ContributesMultibinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.sessionstorage.api.SessionStore
import java.io.File
import javax.inject.Inject
/**
* Create the cache directory for the existing sessions.
*/
@ContributesMultibinding(AppScope::class)
class AppMigration06 @Inject constructor(
private val sessionStore: SessionStore,
@CacheDirectory private val cacheDirectory: File,
) : AppMigration {
override val order: Int = 6
override suspend fun migrate() {
val allSessions = sessionStore.getAllSessions()
for (session in allSessions) {
if (session.cachePath.isEmpty()) {
val sessionFile = File(session.sessionPath)
val sessionFolder = sessionFile.name
val cachePath = File(cacheDirectory, sessionFolder).absolutePath
sessionStore.updateData(session.copy(cachePath = cachePath))
}
}
}
}

View File

@@ -289,6 +289,7 @@ class DefaultBugReporterTest {
slidingSyncProxy = null,
passphrase = null,
sessionPath = "session",
cachePath = "cache",
)
@Test
fun `test sendBugReport error`() = runTest {

View File

@@ -52,5 +52,6 @@ fun aSessionData(
loginType = LoginType.UNKNOWN,
passphrase = null,
sessionPath = "/a/path/to/a/session",
cachePath = "/a/path/to/a/cache",
)
}

View File

@@ -56,6 +56,7 @@ import io.element.android.libraries.matrix.impl.media.RustMediaLoader
import io.element.android.libraries.matrix.impl.notification.RustNotificationService
import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService
import io.element.android.libraries.matrix.impl.oidc.toRustAction
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
import io.element.android.libraries.matrix.impl.pushers.RustPushersService
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
@@ -67,7 +68,7 @@ import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
import io.element.android.libraries.matrix.impl.sync.RustSyncService
import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
import io.element.android.libraries.matrix.impl.util.SessionDirectoryProvider
import io.element.android.libraries.matrix.impl.util.SessionPathsProvider
import io.element.android.libraries.matrix.impl.util.anonymizedTokens
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
@@ -161,7 +162,7 @@ class RustMatrixClient(
sessionDispatcher = sessionDispatcher,
)
private val sessionDirectoryProvider = SessionDirectoryProvider(sessionStore)
private val sessionPathsProvider = SessionPathsProvider(sessionStore)
private val isLoggingOut = AtomicBoolean(false)
@@ -186,7 +187,7 @@ class RustMatrixClient(
isTokenValid = false,
loginType = existingData.loginType,
passphrase = existingData.passphrase,
sessionPath = existingData.sessionPath,
sessionPaths = existingData.getSessionPaths(),
)
sessionStore.updateData(newData)
clientLog.d("Removed session data with access token: '$anonymizedAccessToken'.")
@@ -217,7 +218,7 @@ class RustMatrixClient(
isTokenValid = true,
loginType = existingData.loginType,
passphrase = existingData.passphrase,
sessionPath = existingData.sessionPath,
sessionPaths = existingData.getSessionPaths(),
)
sessionStore.updateData(newData)
clientLog.d("Saved new session data with access token: '$anonymizedAccessToken'.")
@@ -617,16 +618,17 @@ class RustMatrixClient(
private suspend fun File.getCacheSize(
includeCryptoDb: Boolean = false,
): Long = withContext(sessionDispatcher) {
val sessionDirectory = sessionDirectoryProvider.provides(sessionId) ?: return@withContext 0L
val sessionDirectory = sessionPathsProvider.provides(sessionId) ?: return@withContext 0L
val cacheSize = sessionDirectory.cacheDirectory.getSizeOfFiles()
if (includeCryptoDb) {
sessionDirectory.getSizeOfFiles()
cacheSize + sessionDirectory.fileDirectory.getSizeOfFiles()
} else {
listOf(
cacheSize + listOf(
"matrix-sdk-state.sqlite3",
"matrix-sdk-state.sqlite3-shm",
"matrix-sdk-state.sqlite3-wal",
).map { fileName ->
File(sessionDirectory, fileName)
File(sessionDirectory.fileDirectory, fileName)
}.sumOf { file ->
file.length()
}
@@ -636,13 +638,15 @@ class RustMatrixClient(
private suspend fun deleteSessionDirectory(
deleteCryptoDb: Boolean = false,
): Boolean = withContext(sessionDispatcher) {
val sessionDirectory = sessionDirectoryProvider.provides(sessionId) ?: return@withContext false
val sessionPaths = sessionPathsProvider.provides(sessionId) ?: return@withContext false
// Always delete the cache directory
sessionPaths.cacheDirectory.deleteRecursively()
if (deleteCryptoDb) {
// Delete the folder and all its content
sessionDirectory.deleteRecursively()
sessionPaths.fileDirectory.deleteRecursively()
} else {
// Delete only the state.db file
sessionDirectory.listFiles().orEmpty()
sessionPaths.fileDirectory.listFiles().orEmpty()
.filter { it.name.contains("matrix-sdk-state") }
.forEach { file ->
Timber.w("Deleting file ${file.name}...")

View File

@@ -20,6 +20,8 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.matrix.impl.analytics.UtdTracker
import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
import io.element.android.libraries.matrix.impl.util.anonymizedTokens
import io.element.android.libraries.network.useragent.UserAgentProvider
@@ -52,7 +54,7 @@ class RustMatrixClientFactory @Inject constructor(
) {
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
val client = getBaseClientBuilder(
sessionPath = sessionData.sessionPath,
sessionPaths = sessionData.getSessionPaths(),
passphrase = sessionData.passphrase,
slidingSync = if (appPreferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()) {
ClientBuilderSlidingSync.Simplified
@@ -87,14 +89,16 @@ class RustMatrixClientFactory @Inject constructor(
}
internal fun getBaseClientBuilder(
sessionPath: String,
sessionPaths: SessionPaths,
passphrase: String?,
slidingSyncProxy: String? = null,
slidingSync: ClientBuilderSlidingSync,
): ClientBuilder {
return ClientBuilder()
// TODO SDK claims it's valid to use the same path for data and cache, but would be better to use different paths
.sessionPaths(dataPath = sessionPath, cachePath = sessionPath)
.sessionPaths(
dataPath = sessionPaths.fileDirectory.absolutePath,
cachePath = sessionPaths.cacheDirectory.absolutePath,
)
.passphrase(passphrase)
.slidingSyncProxy(slidingSyncProxy)
.userAgent(userAgentProvider.provide())

View File

@@ -21,6 +21,7 @@ import io.element.android.appconfig.AuthenticationConfig
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@@ -31,6 +32,7 @@ import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper
import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData
import io.element.android.libraries.matrix.impl.auth.qrlogin.toStep
@@ -61,6 +63,7 @@ import javax.inject.Inject
@SingleIn(AppScope::class)
class RustMatrixAuthenticationService @Inject constructor(
private val baseDirectory: File,
@CacheDirectory private val cacheDirectory: File,
private val coroutineDispatchers: CoroutineDispatchers,
private val sessionStore: SessionStore,
private val rustMatrixClientFactory: RustMatrixClientFactory,
@@ -73,14 +76,18 @@ class RustMatrixAuthenticationService @Inject constructor(
// Need to keep a copy of the current session path to eventually delete it.
// Ideally it would be possible to get the sessionPath from the Client to avoid doing this.
private var sessionPath: File? = null
private var sessionPaths: SessionPaths? = null
private var currentClient: Client? = null
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
private fun rotateSessionPath(): File {
sessionPath?.deleteRecursively()
return File(baseDirectory, UUID.randomUUID().toString())
.also { sessionPath = it }
private fun rotateSessionPath(): SessionPaths {
sessionPaths?.deleteRecursively()
val subPath = UUID.randomUUID().toString()
return SessionPaths(
fileDirectory = File(baseDirectory, subPath),
cacheDirectory = File(cacheDirectory, subPath),
)
.also { sessionPaths = it }
}
override fun loggedInStateFlow(): Flow<LoggedInState> {
@@ -145,14 +152,14 @@ class RustMatrixAuthenticationService @Inject constructor(
withContext(coroutineDispatchers.io) {
runCatching {
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPath = sessionPath ?: error("You need to call `setHomeserver()` first")
val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first")
client.login(username, password, "Element X Android", null)
val sessionData = client.session()
.toSessionData(
isTokenValid = true,
loginType = LoginType.PASSWORD,
passphrase = pendingPassphrase,
sessionPath = currentSessionPath.absolutePath,
sessionPaths = currentSessionPaths,
)
clear()
sessionStore.storeData(sessionData)
@@ -196,14 +203,14 @@ class RustMatrixAuthenticationService @Inject constructor(
return withContext(coroutineDispatchers.io) {
runCatching {
val client = currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPath = sessionPath ?: error("You need to call `setHomeserver()` first")
val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first")
val urlForOidcLogin = pendingOidcAuthorizationData ?: error("You need to call `getOidcUrl()` first")
client.loginWithOidcCallback(urlForOidcLogin, callbackUrl)
val sessionData = client.session().toSessionData(
isTokenValid = true,
loginType = LoginType.OIDC,
passphrase = pendingPassphrase,
sessionPath = currentSessionPath.absolutePath,
sessionPaths = currentSessionPaths,
)
clear()
pendingOidcAuthorizationData?.close()
@@ -218,10 +225,10 @@ class RustMatrixAuthenticationService @Inject constructor(
override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) =
withContext(coroutineDispatchers.io) {
val emptySessionPath = rotateSessionPath()
val emptySessionPaths = rotateSessionPath()
runCatching {
val client = rustMatrixClientFactory.getBaseClientBuilder(
sessionPath = emptySessionPath.absolutePath,
sessionPaths = emptySessionPaths,
passphrase = pendingPassphrase,
slidingSyncProxy = AuthenticationConfig.SLIDING_SYNC_PROXY_URL,
slidingSync = ClientBuilderSlidingSync.Discovered,
@@ -242,7 +249,7 @@ class RustMatrixAuthenticationService @Inject constructor(
isTokenValid = true,
loginType = LoginType.QR,
passphrase = pendingPassphrase,
sessionPath = emptySessionPath.absolutePath,
sessionPaths = emptySessionPaths,
)
sessionStore.storeData(sessionData)
SessionId(sessionData.userId)
@@ -262,10 +269,10 @@ class RustMatrixAuthenticationService @Inject constructor(
}
private fun getBaseClientBuilder(
sessionPath: File,
sessionPaths: SessionPaths,
) = rustMatrixClientFactory
.getBaseClientBuilder(
sessionPath = sessionPath.absolutePath,
sessionPaths = sessionPaths,
passphrase = pendingPassphrase,
slidingSyncProxy = AuthenticationConfig.SLIDING_SYNC_PROXY_URL,
slidingSync = ClientBuilderSlidingSync.Discovered,

View File

@@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.impl.mapper
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.sessionstorage.api.LoginType
import io.element.android.libraries.sessionstorage.api.SessionData
import org.matrix.rustcomponents.sdk.Session
@@ -25,7 +26,7 @@ internal fun Session.toSessionData(
isTokenValid: Boolean,
loginType: LoginType,
passphrase: String?,
sessionPath: String,
sessionPaths: SessionPaths,
homeserverUrl: String? = null,
) = SessionData(
userId = userId,
@@ -39,5 +40,6 @@ internal fun Session.toSessionData(
isTokenValid = isTokenValid,
loginType = loginType,
passphrase = passphrase,
sessionPath = sessionPath,
sessionPath = sessionPaths.fileDirectory.absolutePath,
cachePath = sessionPaths.cacheDirectory.absolutePath,
)

View File

@@ -0,0 +1,37 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.paths
import io.element.android.libraries.sessionstorage.api.SessionData
import java.io.File
data class SessionPaths(
val fileDirectory: File,
val cacheDirectory: File,
) {
fun deleteRecursively() {
fileDirectory.deleteRecursively()
cacheDirectory.deleteRecursively()
}
}
internal fun SessionData.getSessionPaths(): SessionPaths {
return SessionPaths(
fileDirectory = File(sessionPath),
cacheDirectory = File(cachePath),
)
}

View File

@@ -17,15 +17,15 @@
package io.element.android.libraries.matrix.impl.util
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import io.element.android.libraries.matrix.impl.paths.getSessionPaths
import io.element.android.libraries.sessionstorage.api.SessionStore
import java.io.File
import javax.inject.Inject
class SessionDirectoryProvider @Inject constructor(
class SessionPathsProvider(
private val sessionStore: SessionStore,
) {
suspend fun provides(sessionId: SessionId): File? {
val path = sessionStore.getSession(sessionId.value)?.sessionPath ?: return null
return File(path)
suspend fun provides(sessionId: SessionId): SessionPaths? {
val sessionData = sessionStore.getSession(sessionId.value) ?: return null
return sessionData.getSessionPaths()
}
}

View File

@@ -44,6 +44,8 @@ data class SessionData(
val loginType: LoginType,
/** The optional passphrase used to encrypt data in the SDK local store. */
val passphrase: String?,
/** The path to the session data stored in the filesystem. */
/** The paths to the session data stored in the filesystem. */
val sessionPath: String,
/** The path to the cache data stored for the session in the filesystem. */
val cachePath: String,
)

View File

@@ -35,6 +35,7 @@ internal fun SessionData.toDbModel(): DbSessionData {
loginType = loginType.name,
passphrase = passphrase,
sessionPath = sessionPath,
cachePath = cachePath,
)
}
@@ -52,5 +53,6 @@ internal fun DbSessionData.toApiModel(): SessionData {
loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name),
passphrase = passphrase,
sessionPath = sessionPath,
cachePath = cachePath,
)
}

View File

@@ -25,7 +25,9 @@ CREATE TABLE SessionData (
-- added in version 5
passphrase TEXT,
-- added in version 6
sessionPath TEXT NOT NULL DEFAULT ""
sessionPath TEXT NOT NULL DEFAULT "",
-- added in version 9
cachePath TEXT NOT NULL DEFAULT ""
);

View File

@@ -0,0 +1,4 @@
-- Migrate DB from version 8
-- Add cachePath so we can track the anonymized path for the session cache dir
ALTER TABLE SessionData ADD COLUMN cachePath TEXT NOT NULL DEFAULT "";

View File

@@ -50,6 +50,7 @@ class MainActivity : ComponentActivity() {
val proxyProvider = NoOpProxyProvider()
RustMatrixAuthenticationService(
baseDirectory = baseDirectory,
cacheDirectory = applicationContext.cacheDir,
coroutineDispatchers = Singleton.coroutineDispatchers,
sessionStore = sessionStore,
rustMatrixClientFactory = RustMatrixClientFactory(