Skip to content

Commit

Permalink
Fix issue with DocumentFileCache
Browse files Browse the repository at this point in the history
  • Loading branch information
grote committed Aug 28, 2024
1 parent 0fdc140 commit fc43458
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,23 @@ class SafBackendTest : BackendTest(), KoinComponent {

private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val settingsManager by inject<SettingsManager>()
override val plugin: Backend
get() {
val safStorage = settingsManager.getSafProperties() ?: error("No SAF storage")
val safProperties = SafProperties(
config = safStorage.config,
name = safStorage.name,
isUsb = safStorage.isUsb,
requiresNetwork = safStorage.requiresNetwork,
rootId = safStorage.rootId,
)
return SafBackend(context, safProperties, ".SeedvaultTest")
}
private val safStorage = settingsManager.getSafProperties() ?: error("No SAF storage")
private val safProperties = SafProperties(
config = safStorage.config,
name = safStorage.name,
isUsb = safStorage.isUsb,
requiresNetwork = safStorage.requiresNetwork,
rootId = safStorage.rootId,
)
override val plugin: Backend = SafBackend(context, safProperties, ".SeedvaultTest")

@Test
fun test(): Unit = runBlocking {
fun `test write list read rename delete`(): Unit = runBlocking {
testWriteListReadRenameDelete()
}

@Test
fun `test remove create write file`(): Unit = runBlocking {
testRemoveCreateWriteFile()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor
import android.util.Log
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.isOutOfSpace
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.header.getADForKV
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.backend.isOutOfSpace
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
Expand Down
1 change: 1 addition & 0 deletions app/src/main/resources/simplelogger.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.slf4j.simpleLogger.defaultLogLevel=debug
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,20 @@
package org.calyxos.seedvault.core.backends

import androidx.annotation.VisibleForTesting
import at.bitfire.dav4jvm.exception.HttpException
import org.calyxos.seedvault.core.toHexString
import org.junit.Assert.assertArrayEquals
import kotlin.random.Random
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
import kotlin.test.fail

@VisibleForTesting
public abstract class BackendTest {

public abstract val plugin: Backend

protected suspend fun testWriteListReadRenameDelete() {
try {
plugin.removeAll()
} catch (e: HttpException) {
if (e.code != 404) fail(e.message, e)
}
plugin.removeAll()

val now = System.currentTimeMillis()
val bytes1 = Random.nextBytes(1337)
Expand Down Expand Up @@ -105,4 +99,20 @@ public abstract class BackendTest {
plugin.remove(snapshotNewFolder)
}

protected suspend fun testRemoveCreateWriteFile() {
val now = System.currentTimeMillis()
val blob = LegacyAppBackupFile.Blob(now, Random.nextBytes(32).toHexString())
val bytes = Random.nextBytes(2342)

plugin.remove(blob)
try {
plugin.save(blob).use {
it.write(bytes)
}
assertArrayEquals(bytes, plugin.load(blob as FileHandle).readAllBytes())
} finally {
plugin.remove(blob)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,69 @@ import org.calyxos.seedvault.core.backends.FileBackupFileType
import org.calyxos.seedvault.core.backends.FileHandle
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
import org.calyxos.seedvault.core.backends.TopLevelFolder
import java.util.concurrent.ConcurrentHashMap

internal class DocumentFileCache(
private val context: Context,
private val baseFile: DocumentFile,
private val root: String,
) {

private val cache = mutableMapOf<String, DocumentFile>()
private val cache = ConcurrentHashMap<String, DocumentFile>()

internal suspend fun getRootFile(): DocumentFile {
return cache.getOrPut(root) {
baseFile.getOrCreateDirectory(context, root)
}
}

internal suspend fun getFile(fh: FileHandle): DocumentFile = when (fh) {
internal suspend fun getOrCreateFile(fh: FileHandle): DocumentFile = when (fh) {
is TopLevelFolder -> cache.getOrPut("$root/${fh.relativePath}") {
getRootFile().getOrCreateDirectory(context, fh.name)
}

is LegacyAppBackupFile -> cache.getOrPut("$root/${fh.relativePath}") {
getFile(fh.topLevelFolder).getOrCreateFile(context, fh.name)
}
getOrCreateFile(fh.topLevelFolder).getOrCreateFile(context, fh.name)
}

is FileBackupFileType.Blob -> {
val subFolderName = fh.name.substring(0, 2)
cache.getOrPut("$root/${fh.topLevelFolder.name}/$subFolderName") {
getFile(fh.topLevelFolder).getOrCreateDirectory(context, subFolderName)
getOrCreateFile(fh.topLevelFolder).getOrCreateDirectory(context, subFolderName)
}.getOrCreateFile(context, fh.name)
}

is FileBackupFileType.Snapshot -> {
getFile(fh.topLevelFolder).getOrCreateFile(context, fh.name)
getOrCreateFile(fh.topLevelFolder).getOrCreateFile(context, fh.name)
}
}

internal suspend fun getFile(fh: FileHandle): DocumentFile? = when (fh) {
is TopLevelFolder -> cache.getOrElse("$root/${fh.relativePath}") {
getRootFile().findFileBlocking(context, fh.name)
}

is LegacyAppBackupFile -> cache.getOrElse("$root/${fh.relativePath}") {
getFile(fh.topLevelFolder)?.findFileBlocking(context, fh.name)
}

is FileBackupFileType.Blob -> {
val subFolderName = fh.name.substring(0, 2)
cache.getOrElse("$root/${fh.topLevelFolder.name}/$subFolderName") {
getFile(fh.topLevelFolder)?.findFileBlocking(context, subFolderName)
}?.findFileBlocking(context, fh.name)
}

is FileBackupFileType.Snapshot -> {
getFile(fh.topLevelFolder)?.findFileBlocking(context, fh.name)
}
}

internal fun removeFromCache(fh: FileHandle) {
cache.remove("$root/${fh.relativePath}")
}

internal fun clearAll() {
cache.clear()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import android.provider.DocumentsContract.Root.COLUMN_ROOT_ID
import android.provider.DocumentsContract.renameDocument
import androidx.core.database.getIntOrNull
import androidx.documentfile.provider.DocumentFile
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.Constants.DIRECTORY_ROOT
Expand All @@ -37,6 +38,8 @@ import kotlin.reflect.KClass
internal const val AUTHORITY_STORAGE = "com.android.externalstorage.documents"
internal const val ROOT_ID_DEVICE = "primary"

private const val DEBUG_LOG = true

public class SafBackend(
private val appContext: Context,
private val safProperties: SafProperties,
Expand All @@ -52,10 +55,12 @@ public class SafBackend(
private val cache = DocumentFileCache(context, safProperties.getDocumentFile(context), root)

override suspend fun test(): Boolean {
log.debugLog { "test()" }
return cache.getRootFile().isDirectory
}

override suspend fun getFreeSpace(): Long? {
log.debugLog { "getFreeSpace()" }
val rootId = safProperties.rootId ?: return null
val authority = safProperties.uri.authority
// using DocumentsContract#buildRootUri(String, String) with rootId directly doesn't work
Expand All @@ -82,12 +87,14 @@ public class SafBackend(
}

override suspend fun save(handle: FileHandle): OutputStream {
val file = cache.getFile(handle)
log.debugLog { "save($handle)" }
val file = cache.getOrCreateFile(handle)
return file.getOutputStream(context.contentResolver)
}

override suspend fun load(handle: FileHandle): InputStream {
val file = cache.getFile(handle)
log.debugLog { "load($handle)" }
val file = cache.getOrCreateFile(handle)
return file.getInputStream(context.contentResolver)
}

Expand All @@ -101,10 +108,12 @@ public class SafBackend(
if (LegacyAppBackupFile.IconsFile::class in fileTypes) throw UnsupportedOperationException()
if (LegacyAppBackupFile.Blob::class in fileTypes) throw UnsupportedOperationException()

log.debugLog { "list($topLevelFolder, $fileTypes)" }

val folder = if (topLevelFolder == null) {
cache.getRootFile()
} else {
cache.getFile(topLevelFolder)
cache.getOrCreateFile(topLevelFolder)
}
// limit depth based on wanted types and if top-level folder is given
var depth = if (FileBackupFileType.Blob::class in fileTypes) 3 else 2
Expand Down Expand Up @@ -164,12 +173,16 @@ public class SafBackend(
}

override suspend fun remove(handle: FileHandle) {
val file = cache.getFile(handle)
if (!file.delete()) throw IOException("could not delete ${handle.relativePath}")
log.debugLog { "remove($handle)" }
cache.getFile(handle)?.let { file ->
if (!file.delete()) throw IOException("could not delete ${handle.relativePath}")
cache.removeFromCache(handle)
}
}

override suspend fun rename(from: TopLevelFolder, to: TopLevelFolder) {
val fromFile = cache.getFile(from)
log.debugLog { "rename($from, ${to.name})" }
val fromFile = cache.getOrCreateFile(from)
// don't use fromFile.renameTo(to.name) as that creates "${to.name} (1)"
val newUri = renameDocument(context.contentResolver, fromFile.uri, to.name)
?: throw IOException("could not rename ${from.relativePath}")
Expand All @@ -182,16 +195,28 @@ public class SafBackend(
}

override suspend fun removeAll() {
cache.getRootFile().listFilesBlocking(context).forEach {
it.delete()
log.debugLog { "removeAll()" }
try {
cache.getRootFile().listFilesBlocking(context).forEach { file ->
log.debugLog { " remove ${file.uri}" }
file.delete()
}
} finally {
cache.clearAll()
}
}

override val providerPackageName: String? by lazy {
log.debugLog { "providerPackageName" }
val authority = safProperties.uri.authority ?: return@lazy null
val providerInfo = context.packageManager.resolveContentProvider(authority, 0)
?: return@lazy null
log.debugLog { " ${providerInfo.packageName}" }
providerInfo.packageName
}

}

private inline fun KLogger.debugLog(crossinline block: () -> String) {
if (DEBUG_LOG) debug { block() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@ public class WebDavBackendTest : BackendTest() {
public fun `test write, list, read, rename, delete`(): Unit = runBlocking {
testWriteListReadRenameDelete()
}

@Test
public fun `test remove, create, write file`(): Unit = runBlocking {
testRemoveCreateWriteFile()
}
}

0 comments on commit fc43458

Please sign in to comment.