Prepare backends for new app backup repository
This commit is contained in:
parent
d2df088f2c
commit
c19787a7fa
8 changed files with 228 additions and 63 deletions
|
@ -33,7 +33,7 @@ class SafBackendTest : BackendTest(), KoinComponent {
|
||||||
requiresNetwork = safStorage.requiresNetwork,
|
requiresNetwork = safStorage.requiresNetwork,
|
||||||
rootId = safStorage.rootId,
|
rootId = safStorage.rootId,
|
||||||
)
|
)
|
||||||
override val plugin: Backend = SafBackend(context, safProperties, ".SeedvaultTest")
|
override val backend: Backend = SafBackend(context, safProperties, ".SeedvaultTest")
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test write list read rename delete`(): Unit = runBlocking {
|
fun `test write list read rename delete`(): Unit = runBlocking {
|
||||||
|
|
|
@ -16,26 +16,26 @@ import kotlin.test.assertNotNull
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public abstract class BackendTest {
|
public abstract class BackendTest {
|
||||||
|
|
||||||
public abstract val plugin: Backend
|
public abstract val backend: Backend
|
||||||
|
|
||||||
protected suspend fun testWriteListReadRenameDelete() {
|
protected suspend fun testWriteListReadRenameDelete() {
|
||||||
plugin.removeAll()
|
backend.removeAll()
|
||||||
|
|
||||||
val androidId = "0123456789abcdef"
|
val androidId = "0123456789abcdef"
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val bytes1 = Random.nextBytes(1337)
|
val bytes1 = Random.nextBytes(1337)
|
||||||
val bytes2 = Random.nextBytes(1337 * 8)
|
val bytes2 = Random.nextBytes(1337 * 8)
|
||||||
plugin.save(LegacyAppBackupFile.Metadata(now)).use {
|
backend.save(LegacyAppBackupFile.Metadata(now)).use {
|
||||||
it.write(bytes1)
|
it.write(bytes1)
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin.save(FileBackupFileType.Snapshot(androidId, now)).use {
|
backend.save(FileBackupFileType.Snapshot(androidId, now)).use {
|
||||||
it.write(bytes2)
|
it.write(bytes2)
|
||||||
}
|
}
|
||||||
|
|
||||||
var metadata: LegacyAppBackupFile.Metadata? = null
|
var metadata: LegacyAppBackupFile.Metadata? = null
|
||||||
var snapshot: FileBackupFileType.Snapshot? = null
|
var fileSnapshot: FileBackupFileType.Snapshot? = null
|
||||||
plugin.list(
|
backend.list(
|
||||||
null,
|
null,
|
||||||
FileBackupFileType.Snapshot::class,
|
FileBackupFileType.Snapshot::class,
|
||||||
FileBackupFileType.Blob::class,
|
FileBackupFileType.Blob::class,
|
||||||
|
@ -45,22 +45,22 @@ public abstract class BackendTest {
|
||||||
if (handle is LegacyAppBackupFile.Metadata && handle.token == now) {
|
if (handle is LegacyAppBackupFile.Metadata && handle.token == now) {
|
||||||
metadata = handle
|
metadata = handle
|
||||||
} else if (handle is FileBackupFileType.Snapshot && handle.time == now) {
|
} else if (handle is FileBackupFileType.Snapshot && handle.time == now) {
|
||||||
snapshot = handle
|
fileSnapshot = handle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertNotNull(metadata)
|
assertNotNull(metadata)
|
||||||
assertNotNull(snapshot)
|
assertNotNull(fileSnapshot)
|
||||||
|
|
||||||
assertContentEquals(bytes1, plugin.load(metadata as FileHandle).readAllBytes())
|
assertContentEquals(bytes1, backend.load(metadata as FileHandle).readAllBytes())
|
||||||
assertContentEquals(bytes2, plugin.load(snapshot as FileHandle).readAllBytes())
|
assertContentEquals(bytes2, backend.load(fileSnapshot as FileHandle).readAllBytes())
|
||||||
|
|
||||||
val blobName = Random.nextBytes(32).toHexString()
|
val blobName = Random.nextBytes(32).toHexString()
|
||||||
var blob: FileBackupFileType.Blob? = null
|
var blob: FileBackupFileType.Blob? = null
|
||||||
val bytes3 = Random.nextBytes(1337 * 16)
|
val bytes3 = Random.nextBytes(1337 * 16)
|
||||||
plugin.save(FileBackupFileType.Blob(androidId, blobName)).use {
|
backend.save(FileBackupFileType.Blob(androidId, blobName)).use {
|
||||||
it.write(bytes3)
|
it.write(bytes3)
|
||||||
}
|
}
|
||||||
plugin.list(
|
backend.list(
|
||||||
null,
|
null,
|
||||||
FileBackupFileType.Snapshot::class,
|
FileBackupFileType.Snapshot::class,
|
||||||
FileBackupFileType.Blob::class,
|
FileBackupFileType.Blob::class,
|
||||||
|
@ -72,32 +72,75 @@ public abstract class BackendTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertNotNull(blob)
|
assertNotNull(blob)
|
||||||
assertContentEquals(bytes3, plugin.load(blob as FileHandle).readAllBytes())
|
assertContentEquals(bytes3, backend.load(blob as FileHandle).readAllBytes())
|
||||||
|
|
||||||
// try listing with top-level folder, should find two files of FileBackupFileType in there
|
// try listing with top-level folder, should find two files of FileBackupFileType in there
|
||||||
var numFiles = 0
|
var numFiles = 0
|
||||||
plugin.list(
|
backend.list(
|
||||||
snapshot!!.topLevelFolder,
|
fileSnapshot!!.topLevelFolder,
|
||||||
FileBackupFileType.Snapshot::class,
|
FileBackupFileType.Snapshot::class,
|
||||||
FileBackupFileType.Blob::class,
|
FileBackupFileType.Blob::class,
|
||||||
LegacyAppBackupFile.Metadata::class,
|
LegacyAppBackupFile.Metadata::class,
|
||||||
) { numFiles++ }
|
) { numFiles++ }
|
||||||
assertEquals(2, numFiles)
|
assertEquals(2, numFiles)
|
||||||
|
|
||||||
plugin.remove(snapshot as FileHandle)
|
val repoId = Random.nextBytes(32).toHexString()
|
||||||
|
val snapshotId = Random.nextBytes(32).toHexString()
|
||||||
|
val blobId = Random.nextBytes(32).toHexString()
|
||||||
|
|
||||||
|
val bytes4 = Random.nextBytes(1337)
|
||||||
|
val bytes5 = Random.nextBytes(1337 * 8)
|
||||||
|
backend.save(AppBackupFileType.Snapshot(repoId, snapshotId)).use {
|
||||||
|
it.write(bytes4)
|
||||||
|
}
|
||||||
|
|
||||||
|
var appSnapshot: AppBackupFileType.Snapshot? = null
|
||||||
|
backend.list(
|
||||||
|
null,
|
||||||
|
AppBackupFileType.Snapshot::class,
|
||||||
|
) { fileInfo ->
|
||||||
|
val handle = fileInfo.fileHandle
|
||||||
|
if (handle is AppBackupFileType.Snapshot) {
|
||||||
|
appSnapshot = handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertNotNull(appSnapshot)
|
||||||
|
assertContentEquals(bytes4, backend.load(appSnapshot as FileHandle).readAllBytes())
|
||||||
|
|
||||||
|
backend.save(AppBackupFileType.Blob(repoId, blobId)).use {
|
||||||
|
it.write(bytes5)
|
||||||
|
}
|
||||||
|
|
||||||
|
var blobHandle: AppBackupFileType.Blob? = null
|
||||||
|
backend.list(
|
||||||
|
TopLevelFolder(repoId),
|
||||||
|
AppBackupFileType.Blob::class,
|
||||||
|
LegacyAppBackupFile.Metadata::class,
|
||||||
|
) { fileInfo ->
|
||||||
|
val handle = fileInfo.fileHandle
|
||||||
|
if (handle is AppBackupFileType.Blob) {
|
||||||
|
blobHandle = handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertNotNull(blobHandle)
|
||||||
|
assertContentEquals(bytes5, backend.load(blobHandle as FileHandle).readAllBytes())
|
||||||
|
|
||||||
|
backend.remove(fileSnapshot as FileHandle)
|
||||||
|
backend.remove(appSnapshot as FileHandle)
|
||||||
|
backend.remove(blobHandle as FileHandle)
|
||||||
|
|
||||||
// rename snapshots
|
// rename snapshots
|
||||||
val snapshotNewFolder = TopLevelFolder("a123456789abcdef.sv")
|
val snapshotNewFolder = TopLevelFolder("a123456789abcdef.sv")
|
||||||
plugin.rename(snapshot!!.topLevelFolder, snapshotNewFolder)
|
backend.rename(fileSnapshot!!.topLevelFolder, snapshotNewFolder)
|
||||||
|
|
||||||
// rename to existing folder should fail
|
// rename to existing folder should fail
|
||||||
val e = assertFailsWith<Exception> {
|
val e = assertFailsWith<Exception> {
|
||||||
plugin.rename(snapshotNewFolder, metadata!!.topLevelFolder)
|
backend.rename(snapshotNewFolder, metadata!!.topLevelFolder)
|
||||||
}
|
}
|
||||||
println(e)
|
println(e)
|
||||||
|
|
||||||
plugin.remove(metadata!!.topLevelFolder)
|
backend.remove(metadata!!.topLevelFolder)
|
||||||
plugin.remove(snapshotNewFolder)
|
backend.remove(snapshotNewFolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected suspend fun testRemoveCreateWriteFile() {
|
protected suspend fun testRemoveCreateWriteFile() {
|
||||||
|
@ -105,14 +148,14 @@ public abstract class BackendTest {
|
||||||
val blob = LegacyAppBackupFile.Blob(now, Random.nextBytes(32).toHexString())
|
val blob = LegacyAppBackupFile.Blob(now, Random.nextBytes(32).toHexString())
|
||||||
val bytes = Random.nextBytes(2342)
|
val bytes = Random.nextBytes(2342)
|
||||||
|
|
||||||
plugin.remove(blob)
|
backend.remove(blob)
|
||||||
try {
|
try {
|
||||||
plugin.save(blob).use {
|
backend.save(blob).use {
|
||||||
it.write(bytes)
|
it.write(bytes)
|
||||||
}
|
}
|
||||||
assertContentEquals(bytes, plugin.load(blob as FileHandle).readAllBytes())
|
assertContentEquals(bytes, backend.load(blob as FileHandle).readAllBytes())
|
||||||
} finally {
|
} finally {
|
||||||
plugin.remove(blob)
|
backend.remove(blob)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,12 +10,18 @@ public object Constants {
|
||||||
public const val DIRECTORY_ROOT: String = ".SeedVaultAndroidBackup"
|
public const val DIRECTORY_ROOT: String = ".SeedVaultAndroidBackup"
|
||||||
internal const val FILE_BACKUP_METADATA = ".backup.metadata"
|
internal const val FILE_BACKUP_METADATA = ".backup.metadata"
|
||||||
internal const val FILE_BACKUP_ICONS = ".backup.icons"
|
internal const val FILE_BACKUP_ICONS = ".backup.icons"
|
||||||
public val tokenRegex: Regex = Regex("([0-9]{13})") // good until the year 2286
|
public val tokenRegex: Regex = Regex("^([0-9]{13})$") // good until the year 2286
|
||||||
public const val SNAPSHOT_EXT: String = ".SeedSnap"
|
public const val APP_SNAPSHOT_EXT: String = ".snapshot"
|
||||||
public val folderRegex: Regex = Regex("^[a-f0-9]{16}\\.sv$")
|
public const val FILE_SNAPSHOT_EXT: String = ".SeedSnap"
|
||||||
public val chunkFolderRegex: Regex = Regex("[a-f0-9]{2}")
|
public val repoIdRegex: Regex = Regex("^[a-f0-9]{64}$")
|
||||||
public val chunkRegex: Regex = Regex("[a-f0-9]{64}")
|
public val fileFolderRegex: Regex = Regex("^[a-f0-9]{16}\\.sv$")
|
||||||
public val snapshotRegex: Regex = Regex("([0-9]{13})\\.SeedSnap") // good until the year 2286
|
public val chunkFolderRegex: Regex = Regex("^[a-f0-9]{2}$")
|
||||||
|
public val blobFolderRegex: Regex = chunkFolderRegex
|
||||||
|
public val chunkRegex: Regex = repoIdRegex
|
||||||
|
public val blobRegex: Regex = repoIdRegex
|
||||||
|
// good until year 2286
|
||||||
|
public val appSnapshotRegex: Regex = Regex("(^[a-f0-9]{64})\\.snapshot$")
|
||||||
|
public val fileSnapshotRegex: Regex = Regex("(^[0-9]{13})\\.SeedSnap$") // good until year 2286
|
||||||
public const val MIME_TYPE: String = "application/octet-stream"
|
public const val MIME_TYPE: String = "application/octet-stream"
|
||||||
public const val CHUNK_FOLDER_COUNT: Int = 256
|
public const val CHUNK_FOLDER_COUNT: Int = 256
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,10 @@
|
||||||
|
|
||||||
package org.calyxos.seedvault.core.backends
|
package org.calyxos.seedvault.core.backends
|
||||||
|
|
||||||
|
import org.calyxos.seedvault.core.backends.Constants.APP_SNAPSHOT_EXT
|
||||||
import org.calyxos.seedvault.core.backends.Constants.FILE_BACKUP_ICONS
|
import org.calyxos.seedvault.core.backends.Constants.FILE_BACKUP_ICONS
|
||||||
import org.calyxos.seedvault.core.backends.Constants.FILE_BACKUP_METADATA
|
import org.calyxos.seedvault.core.backends.Constants.FILE_BACKUP_METADATA
|
||||||
import org.calyxos.seedvault.core.backends.Constants.SNAPSHOT_EXT
|
import org.calyxos.seedvault.core.backends.Constants.FILE_SNAPSHOT_EXT
|
||||||
|
|
||||||
public sealed class FileHandle {
|
public sealed class FileHandle {
|
||||||
public abstract val name: String
|
public abstract val name: String
|
||||||
|
@ -68,11 +69,32 @@ public sealed class FileBackupFileType : FileHandle() {
|
||||||
override val androidId: String,
|
override val androidId: String,
|
||||||
val time: Long,
|
val time: Long,
|
||||||
) : FileBackupFileType() {
|
) : FileBackupFileType() {
|
||||||
override val name: String = "$time$SNAPSHOT_EXT"
|
override val name: String = "$time$FILE_SNAPSHOT_EXT"
|
||||||
override val relativePath: String get() = "$androidId.sv/$name"
|
override val relativePath: String get() = "$androidId.sv/$name"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class AppBackupFileType : FileHandle() {
|
||||||
|
public abstract val repoId: String
|
||||||
|
|
||||||
|
public val topLevelFolder: TopLevelFolder get() = TopLevelFolder(repoId)
|
||||||
|
|
||||||
|
public data class Blob(
|
||||||
|
override val repoId: String,
|
||||||
|
override val name: String,
|
||||||
|
) : AppBackupFileType() {
|
||||||
|
override val relativePath: String get() = "$repoId/${name.substring(0, 2)}/$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
public data class Snapshot(
|
||||||
|
override val repoId: String,
|
||||||
|
val hash: String,
|
||||||
|
) : AppBackupFileType() {
|
||||||
|
override val name: String = "$hash$APP_SNAPSHOT_EXT"
|
||||||
|
override val relativePath: String get() = "$repoId/$name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public data class FileInfo(
|
public data class FileInfo(
|
||||||
val fileHandle: FileHandle,
|
val fileHandle: FileHandle,
|
||||||
val size: Long,
|
val size: Long,
|
||||||
|
|
|
@ -7,6 +7,7 @@ package org.calyxos.seedvault.core.backends.saf
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import org.calyxos.seedvault.core.backends.AppBackupFileType
|
||||||
import org.calyxos.seedvault.core.backends.FileBackupFileType
|
import org.calyxos.seedvault.core.backends.FileBackupFileType
|
||||||
import org.calyxos.seedvault.core.backends.FileHandle
|
import org.calyxos.seedvault.core.backends.FileHandle
|
||||||
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
|
import org.calyxos.seedvault.core.backends.LegacyAppBackupFile
|
||||||
|
@ -32,7 +33,14 @@ internal class DocumentFileCache(
|
||||||
getRootFile().getOrCreateDirectory(context, fh.name)
|
getRootFile().getOrCreateDirectory(context, fh.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
is LegacyAppBackupFile -> cache.getOrPut("$root/${fh.relativePath}") {
|
is AppBackupFileType.Blob -> {
|
||||||
|
val subFolderName = fh.name.substring(0, 2)
|
||||||
|
cache.getOrPut("$root/${fh.topLevelFolder.name}/$subFolderName") {
|
||||||
|
getOrCreateFile(fh.topLevelFolder).getOrCreateDirectory(context, subFolderName)
|
||||||
|
}.getOrCreateFile(context, fh.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppBackupFileType.Snapshot -> {
|
||||||
getOrCreateFile(fh.topLevelFolder).getOrCreateFile(context, fh.name)
|
getOrCreateFile(fh.topLevelFolder).getOrCreateFile(context, fh.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +54,10 @@ internal class DocumentFileCache(
|
||||||
is FileBackupFileType.Snapshot -> {
|
is FileBackupFileType.Snapshot -> {
|
||||||
getOrCreateFile(fh.topLevelFolder).getOrCreateFile(context, fh.name)
|
getOrCreateFile(fh.topLevelFolder).getOrCreateFile(context, fh.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is LegacyAppBackupFile -> cache.getOrPut("$root/${fh.relativePath}") {
|
||||||
|
getOrCreateFile(fh.topLevelFolder).getOrCreateFile(context, fh.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun getFile(fh: FileHandle): DocumentFile? = when (fh) {
|
internal suspend fun getFile(fh: FileHandle): DocumentFile? = when (fh) {
|
||||||
|
@ -53,7 +65,14 @@ internal class DocumentFileCache(
|
||||||
getRootFile().findFileBlocking(context, fh.name)
|
getRootFile().findFileBlocking(context, fh.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
is LegacyAppBackupFile -> cache.getOrElse("$root/${fh.relativePath}") {
|
is AppBackupFileType.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 AppBackupFileType.Snapshot -> {
|
||||||
getFile(fh.topLevelFolder)?.findFileBlocking(context, fh.name)
|
getFile(fh.topLevelFolder)?.findFileBlocking(context, fh.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,6 +86,10 @@ internal class DocumentFileCache(
|
||||||
is FileBackupFileType.Snapshot -> {
|
is FileBackupFileType.Snapshot -> {
|
||||||
getFile(fh.topLevelFolder)?.findFileBlocking(context, fh.name)
|
getFile(fh.topLevelFolder)?.findFileBlocking(context, fh.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is LegacyAppBackupFile -> cache.getOrElse("$root/${fh.relativePath}") {
|
||||||
|
getFile(fh.topLevelFolder)?.findFileBlocking(context, fh.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun removeFromCache(fh: FileHandle) {
|
internal fun removeFromCache(fh: FileHandle) {
|
||||||
|
|
|
@ -16,13 +16,18 @@ import androidx.core.database.getIntOrNull
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import io.github.oshai.kotlinlogging.KLogger
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.calyxos.seedvault.core.backends.AppBackupFileType
|
||||||
import org.calyxos.seedvault.core.backends.Backend
|
import org.calyxos.seedvault.core.backends.Backend
|
||||||
import org.calyxos.seedvault.core.backends.Constants.DIRECTORY_ROOT
|
import org.calyxos.seedvault.core.backends.Constants.DIRECTORY_ROOT
|
||||||
import org.calyxos.seedvault.core.backends.Constants.FILE_BACKUP_METADATA
|
import org.calyxos.seedvault.core.backends.Constants.FILE_BACKUP_METADATA
|
||||||
|
import org.calyxos.seedvault.core.backends.Constants.appSnapshotRegex
|
||||||
|
import org.calyxos.seedvault.core.backends.Constants.blobFolderRegex
|
||||||
|
import org.calyxos.seedvault.core.backends.Constants.blobRegex
|
||||||
import org.calyxos.seedvault.core.backends.Constants.chunkFolderRegex
|
import org.calyxos.seedvault.core.backends.Constants.chunkFolderRegex
|
||||||
import org.calyxos.seedvault.core.backends.Constants.chunkRegex
|
import org.calyxos.seedvault.core.backends.Constants.chunkRegex
|
||||||
import org.calyxos.seedvault.core.backends.Constants.folderRegex
|
import org.calyxos.seedvault.core.backends.Constants.fileFolderRegex
|
||||||
import org.calyxos.seedvault.core.backends.Constants.snapshotRegex
|
import org.calyxos.seedvault.core.backends.Constants.fileSnapshotRegex
|
||||||
|
import org.calyxos.seedvault.core.backends.Constants.repoIdRegex
|
||||||
import org.calyxos.seedvault.core.backends.Constants.tokenRegex
|
import org.calyxos.seedvault.core.backends.Constants.tokenRegex
|
||||||
import org.calyxos.seedvault.core.backends.FileBackupFileType
|
import org.calyxos.seedvault.core.backends.FileBackupFileType
|
||||||
import org.calyxos.seedvault.core.backends.FileHandle
|
import org.calyxos.seedvault.core.backends.FileHandle
|
||||||
|
@ -103,7 +108,7 @@ public class SafBackend(
|
||||||
if (LegacyAppBackupFile.IconsFile::class in fileTypes) throw UnsupportedOperationException()
|
if (LegacyAppBackupFile.IconsFile::class in fileTypes) throw UnsupportedOperationException()
|
||||||
if (LegacyAppBackupFile.Blob::class in fileTypes) throw UnsupportedOperationException()
|
if (LegacyAppBackupFile.Blob::class in fileTypes) throw UnsupportedOperationException()
|
||||||
|
|
||||||
log.debugLog { "list($topLevelFolder, $fileTypes)" }
|
log.debugLog { "list($topLevelFolder, ${fileTypes.map { it.simpleName }})" }
|
||||||
|
|
||||||
val folder = if (topLevelFolder == null) {
|
val folder = if (topLevelFolder == null) {
|
||||||
cache.getRootFile()
|
cache.getRootFile()
|
||||||
|
@ -111,23 +116,45 @@ public class SafBackend(
|
||||||
cache.getOrCreateFile(topLevelFolder)
|
cache.getOrCreateFile(topLevelFolder)
|
||||||
}
|
}
|
||||||
// limit depth based on wanted types and if top-level folder is given
|
// limit depth based on wanted types and if top-level folder is given
|
||||||
var depth = if (FileBackupFileType.Blob::class in fileTypes) 3 else 2
|
var depth = if (FileBackupFileType.Blob::class in fileTypes ||
|
||||||
|
AppBackupFileType.Blob::class in fileTypes
|
||||||
|
) 3 else 2
|
||||||
if (topLevelFolder != null) depth -= 1
|
if (topLevelFolder != null) depth -= 1
|
||||||
|
|
||||||
folder.listFilesRecursive(depth) { file ->
|
folder.listFilesRecursive(depth) { file ->
|
||||||
if (!file.isFile) return@listFilesRecursive
|
if (!file.isFile) return@listFilesRecursive
|
||||||
val parentName = file.parentFile?.name ?: return@listFilesRecursive
|
val parentName = file.parentFile?.name ?: return@listFilesRecursive
|
||||||
val name = file.name ?: return@listFilesRecursive
|
val name = file.name ?: return@listFilesRecursive
|
||||||
if (LegacyAppBackupFile.Metadata::class in fileTypes && name == FILE_BACKUP_METADATA &&
|
if (AppBackupFileType.Snapshot::class in fileTypes ||
|
||||||
parentName.matches(tokenRegex)
|
AppBackupFileType::class in fileTypes
|
||||||
) {
|
) {
|
||||||
val metadata = LegacyAppBackupFile.Metadata(parentName.toLong())
|
val match = appSnapshotRegex.matchEntire(name)
|
||||||
callback(FileInfo(metadata, file.length()))
|
if (match != null && repoIdRegex.matches(parentName)) {
|
||||||
|
val snapshot = AppBackupFileType.Snapshot(
|
||||||
|
repoId = parentName,
|
||||||
|
hash = match.groupValues[1],
|
||||||
|
)
|
||||||
|
callback(FileInfo(snapshot, file.length()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((AppBackupFileType.Blob::class in fileTypes ||
|
||||||
|
AppBackupFileType::class in fileTypes)
|
||||||
|
) {
|
||||||
|
val repoId = file.parentFile?.parentFile?.name ?: ""
|
||||||
|
if (repoIdRegex.matches(repoId) && blobFolderRegex.matches(parentName)) {
|
||||||
|
if (blobRegex.matches(name)) {
|
||||||
|
val blob = AppBackupFileType.Blob(
|
||||||
|
repoId = repoId,
|
||||||
|
name = name,
|
||||||
|
)
|
||||||
|
callback(FileInfo(blob, file.length()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (FileBackupFileType.Snapshot::class in fileTypes ||
|
if (FileBackupFileType.Snapshot::class in fileTypes ||
|
||||||
FileBackupFileType::class in fileTypes
|
FileBackupFileType::class in fileTypes
|
||||||
) {
|
) {
|
||||||
val match = snapshotRegex.matchEntire(name)
|
val match = fileSnapshotRegex.matchEntire(name)
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
val snapshot = FileBackupFileType.Snapshot(
|
val snapshot = FileBackupFileType.Snapshot(
|
||||||
androidId = parentName.substringBefore('.'),
|
androidId = parentName.substringBefore('.'),
|
||||||
|
@ -140,7 +167,7 @@ public class SafBackend(
|
||||||
FileBackupFileType::class in fileTypes)
|
FileBackupFileType::class in fileTypes)
|
||||||
) {
|
) {
|
||||||
val androidIdSv = file.parentFile?.parentFile?.name ?: ""
|
val androidIdSv = file.parentFile?.parentFile?.name ?: ""
|
||||||
if (folderRegex.matches(androidIdSv) && chunkFolderRegex.matches(parentName)) {
|
if (fileFolderRegex.matches(androidIdSv) && chunkFolderRegex.matches(parentName)) {
|
||||||
if (chunkRegex.matches(name)) {
|
if (chunkRegex.matches(name)) {
|
||||||
val blob = FileBackupFileType.Blob(
|
val blob = FileBackupFileType.Blob(
|
||||||
androidId = androidIdSv.substringBefore('.'),
|
androidId = androidIdSv.substringBefore('.'),
|
||||||
|
@ -150,6 +177,12 @@ public class SafBackend(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (LegacyAppBackupFile.Metadata::class in fileTypes && name == FILE_BACKUP_METADATA &&
|
||||||
|
parentName.matches(tokenRegex)
|
||||||
|
) {
|
||||||
|
val metadata = LegacyAppBackupFile.Metadata(parentName.toLong())
|
||||||
|
callback(FileInfo(metadata, file.length()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,16 +206,18 @@ public class SafBackend(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun rename(from: TopLevelFolder, to: TopLevelFolder) {
|
override suspend fun rename(from: TopLevelFolder, to: TopLevelFolder) {
|
||||||
log.debugLog { "rename($from, ${to.name})" }
|
val toName = to.name // querying name is expensive
|
||||||
|
log.debugLog { "rename($from, $toName)" }
|
||||||
val fromFile = cache.getOrCreateFile(from)
|
val fromFile = cache.getOrCreateFile(from)
|
||||||
// don't use fromFile.renameTo(to.name) as that creates "${to.name} (1)"
|
// don't use fromFile.renameTo(to.name) as that creates "${to.name} (1)"
|
||||||
val newUri = renameDocument(context.contentResolver, fromFile.uri, to.name)
|
val newUri = renameDocument(context.contentResolver, fromFile.uri, toName)
|
||||||
?: throw IOException("could not rename ${from.relativePath}")
|
?: throw IOException("could not rename ${from.relativePath}")
|
||||||
|
cache.removeFromCache(from) // after renaming cached file isn't valid anymore
|
||||||
val toFile = DocumentFile.fromTreeUri(context, newUri)
|
val toFile = DocumentFile.fromTreeUri(context, newUri)
|
||||||
?: throw IOException("renamed URI invalid: $newUri")
|
?: throw IOException("renamed URI invalid: $newUri")
|
||||||
if (toFile.name != to.name) {
|
if (toFile.name != toName) {
|
||||||
toFile.delete()
|
toFile.delete()
|
||||||
throw IOException("renamed to ${toFile.name}, but expected ${to.name}")
|
throw IOException("renamed to ${toFile.name}, but expected $toName")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,13 +26,17 @@ import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okio.BufferedSink
|
import okio.BufferedSink
|
||||||
|
import org.calyxos.seedvault.core.backends.AppBackupFileType
|
||||||
import org.calyxos.seedvault.core.backends.Backend
|
import org.calyxos.seedvault.core.backends.Backend
|
||||||
import org.calyxos.seedvault.core.backends.Constants.DIRECTORY_ROOT
|
import org.calyxos.seedvault.core.backends.Constants.DIRECTORY_ROOT
|
||||||
import org.calyxos.seedvault.core.backends.Constants.FILE_BACKUP_METADATA
|
import org.calyxos.seedvault.core.backends.Constants.FILE_BACKUP_METADATA
|
||||||
|
import org.calyxos.seedvault.core.backends.Constants.appSnapshotRegex
|
||||||
|
import org.calyxos.seedvault.core.backends.Constants.blobFolderRegex
|
||||||
import org.calyxos.seedvault.core.backends.Constants.chunkFolderRegex
|
import org.calyxos.seedvault.core.backends.Constants.chunkFolderRegex
|
||||||
import org.calyxos.seedvault.core.backends.Constants.chunkRegex
|
import org.calyxos.seedvault.core.backends.Constants.chunkRegex
|
||||||
import org.calyxos.seedvault.core.backends.Constants.folderRegex
|
import org.calyxos.seedvault.core.backends.Constants.fileFolderRegex
|
||||||
import org.calyxos.seedvault.core.backends.Constants.snapshotRegex
|
import org.calyxos.seedvault.core.backends.Constants.fileSnapshotRegex
|
||||||
|
import org.calyxos.seedvault.core.backends.Constants.repoIdRegex
|
||||||
import org.calyxos.seedvault.core.backends.Constants.tokenRegex
|
import org.calyxos.seedvault.core.backends.Constants.tokenRegex
|
||||||
import org.calyxos.seedvault.core.backends.FileBackupFileType
|
import org.calyxos.seedvault.core.backends.FileBackupFileType
|
||||||
import org.calyxos.seedvault.core.backends.FileHandle
|
import org.calyxos.seedvault.core.backends.FileHandle
|
||||||
|
@ -180,7 +184,9 @@ public class WebDavBackend(
|
||||||
if (LegacyAppBackupFile.Blob::class in fileTypes) throw UnsupportedOperationException()
|
if (LegacyAppBackupFile.Blob::class in fileTypes) throw UnsupportedOperationException()
|
||||||
|
|
||||||
// limit depth based on wanted types and if top-level folder is given
|
// limit depth based on wanted types and if top-level folder is given
|
||||||
var depth = if (FileBackupFileType.Blob::class in fileTypes) 3 else 2
|
var depth = if (FileBackupFileType.Blob::class in fileTypes ||
|
||||||
|
AppBackupFileType.Blob::class in fileTypes
|
||||||
|
) 3 else 2
|
||||||
if (topLevelFolder != null) depth -= 1
|
if (topLevelFolder != null) depth -= 1
|
||||||
|
|
||||||
val location = if (topLevelFolder == null) {
|
val location = if (topLevelFolder == null) {
|
||||||
|
@ -204,19 +210,40 @@ public class WebDavBackend(
|
||||||
val name = response.hrefName()
|
val name = response.hrefName()
|
||||||
val parentName = response.href.pathSegments[response.href.pathSegments.size - 2]
|
val parentName = response.href.pathSegments[response.href.pathSegments.size - 2]
|
||||||
|
|
||||||
if (LegacyAppBackupFile.Metadata::class in fileTypes) {
|
if (AppBackupFileType.Snapshot::class in fileTypes ||
|
||||||
if (name == FILE_BACKUP_METADATA && parentName.matches(tokenRegex)) {
|
AppBackupFileType::class in fileTypes
|
||||||
val metadata = LegacyAppBackupFile.Metadata(parentName.toLong())
|
) {
|
||||||
|
val match = appSnapshotRegex.matchEntire(name)
|
||||||
|
if (match != null && repoIdRegex.matches(parentName)) {
|
||||||
val size = response.properties.contentLength()
|
val size = response.properties.contentLength()
|
||||||
callback(FileInfo(metadata, size))
|
val snapshot = AppBackupFileType.Snapshot(
|
||||||
// we can find .backup.metadata files, so no need for nginx workaround
|
repoId = parentName,
|
||||||
tokenFolders.clear()
|
hash = match.groupValues[1],
|
||||||
|
)
|
||||||
|
callback(FileInfo(snapshot, size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((AppBackupFileType.Blob::class in fileTypes ||
|
||||||
|
AppBackupFileType::class in fileTypes) && response.href.pathSize >= 3
|
||||||
|
) {
|
||||||
|
val repoId = response.href.pathSegments[response.href.pathSegments.size - 3]
|
||||||
|
if (repoIdRegex.matches(repoId) &&
|
||||||
|
blobFolderRegex.matches(parentName)
|
||||||
|
) {
|
||||||
|
if (chunkRegex.matches(name)) {
|
||||||
|
val blob = AppBackupFileType.Blob(
|
||||||
|
repoId = repoId,
|
||||||
|
name = name,
|
||||||
|
)
|
||||||
|
val size = response.properties.contentLength()
|
||||||
|
callback(FileInfo(blob, size))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (FileBackupFileType.Snapshot::class in fileTypes ||
|
if (FileBackupFileType.Snapshot::class in fileTypes ||
|
||||||
FileBackupFileType::class in fileTypes
|
FileBackupFileType::class in fileTypes
|
||||||
) {
|
) {
|
||||||
val match = snapshotRegex.matchEntire(name)
|
val match = fileSnapshotRegex.matchEntire(name)
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
val size = response.properties.contentLength()
|
val size = response.properties.contentLength()
|
||||||
val snapshot = FileBackupFileType.Snapshot(
|
val snapshot = FileBackupFileType.Snapshot(
|
||||||
|
@ -231,7 +258,7 @@ public class WebDavBackend(
|
||||||
) {
|
) {
|
||||||
val androidIdSv =
|
val androidIdSv =
|
||||||
response.href.pathSegments[response.href.pathSegments.size - 3]
|
response.href.pathSegments[response.href.pathSegments.size - 3]
|
||||||
if (folderRegex.matches(androidIdSv) &&
|
if (fileFolderRegex.matches(androidIdSv) &&
|
||||||
chunkFolderRegex.matches(parentName)
|
chunkFolderRegex.matches(parentName)
|
||||||
) {
|
) {
|
||||||
if (chunkRegex.matches(name)) {
|
if (chunkRegex.matches(name)) {
|
||||||
|
@ -244,6 +271,15 @@ public class WebDavBackend(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (LegacyAppBackupFile.Metadata::class in fileTypes) {
|
||||||
|
if (name == FILE_BACKUP_METADATA && parentName.matches(tokenRegex)) {
|
||||||
|
val metadata = LegacyAppBackupFile.Metadata(parentName.toLong())
|
||||||
|
val size = response.properties.contentLength()
|
||||||
|
callback(FileInfo(metadata, size))
|
||||||
|
// we can find .backup.metadata files, so no need for nginx workaround
|
||||||
|
tokenFolders.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: NotFoundException) {
|
} catch (e: NotFoundException) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import org.calyxos.seedvault.core.backends.BackendTest
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
public class WebDavBackendTest : BackendTest() {
|
public class WebDavBackendTest : BackendTest() {
|
||||||
override val plugin: Backend = WebDavBackend(WebDavTestConfig.getConfig(), ".SeedvaultTest")
|
override val backend: Backend = WebDavBackend(WebDavTestConfig.getConfig(), ".SeedvaultTest")
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public fun `test write, list, read, rename, delete`(): Unit = runBlocking {
|
public fun `test write, list, read, rename, delete`(): Unit = runBlocking {
|
||||||
|
|
Loading…
Reference in a new issue