Skip to content

Commit

Permalink
WIP Files Backup Checker
Browse files Browse the repository at this point in the history
  • Loading branch information
grote committed Nov 13, 2024
1 parent 5891024 commit 09adff7
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import org.calyxos.backup.storage.SnapshotRetriever
import org.calyxos.backup.storage.backup.Backup
import org.calyxos.backup.storage.backup.BackupSnapshot
import org.calyxos.backup.storage.backup.ChunksCacheRepopulater
import org.calyxos.backup.storage.check.Checker
import org.calyxos.backup.storage.db.Db
import org.calyxos.backup.storage.getCurrentBackupSnapshots
import org.calyxos.backup.storage.getMediaType
Expand Down Expand Up @@ -88,6 +89,9 @@ public class StorageBackup(
private val pruner by lazy {
Pruner(db, retention, pluginGetter, androidId, keyManager, snapshotRetriever)
}
private val checker by lazy {
Checker(db, pluginGetter, snapshotRetriever, keyManager, chunksCacheRepopulater, androidId)
}

private val backupRunning = AtomicBoolean(false)
private val restoreRunning = AtomicBoolean(false)
Expand Down Expand Up @@ -245,4 +249,11 @@ public class StorageBackup(
}
}

public suspend fun checkBackups(percent: Int, backupObserver: BackupObserver?) {
check(percent in 0..100) { "Invalid percentage: $percent" }
withContext(dispatcher) {
checker.check(percent, backupObserver)
}
}

}
147 changes: 147 additions & 0 deletions storage/lib/src/main/java/org/calyxos/backup/storage/check/Checker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/

package org.calyxos.backup.storage.check

import android.util.Log
import com.google.protobuf.InvalidProtocolBufferException
import org.calyxos.backup.storage.SnapshotRetriever
import org.calyxos.backup.storage.api.BackupObserver
import org.calyxos.backup.storage.api.StoredSnapshot
import org.calyxos.backup.storage.backup.Backup.Companion.VERSION
import org.calyxos.backup.storage.backup.ChunksCacheRepopulater
import org.calyxos.backup.storage.crypto.ChunkCrypto
import org.calyxos.backup.storage.crypto.StreamCrypto
import org.calyxos.backup.storage.db.Db
import org.calyxos.backup.storage.restore.readVersion
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.FileBackupFileType
import org.calyxos.seedvault.core.backends.TopLevelFolder
import org.calyxos.seedvault.core.crypto.KeyManager
import org.calyxos.seedvault.core.toHexString
import java.io.IOException
import java.security.GeneralSecurityException
import kotlin.math.roundToLong

private val TAG = Checker::class.simpleName

internal class Checker(
private val db: Db,
private val backendGetter: () -> Backend,
private val snapshotRetriever: SnapshotRetriever,
private val keyManager: KeyManager,
private val cacheRepopulater: ChunksCacheRepopulater,
private val androidId: String,
private val streamCrypto: StreamCrypto = StreamCrypto,
private val chunkCrypto: ChunkCrypto = ChunkCrypto,
) {

private val backend get() = backendGetter()

private val streamKey
get() = try {
streamCrypto.deriveStreamKey(keyManager.getMainKey())
} catch (e: GeneralSecurityException) {
throw AssertionError(e)
}
private val mac
get() = try {
chunkCrypto.getMac(ChunkCrypto.deriveChunkIdKey(keyManager.getMainKey()))
} catch (e: GeneralSecurityException) {
throw AssertionError(e)
}

fun getBackupSize(): Long {
return db.getChunksCache().getSizeOfCachedChunks()
}

@Throws(
IOException::class,
GeneralSecurityException::class,
InvalidProtocolBufferException::class,
)
suspend fun check(percent: Int, backupObserver: BackupObserver?) {
check(percent in 0..100) { "Invalid percentage: $percent" }

// get all snapshots and blobs on storage
val topLevelFolder = TopLevelFolder("$androidId.sv")
val storedSnapshots = mutableListOf<StoredSnapshot>()
val availableChunkIds = mutableMapOf<String, Long>()
backend.list(
topLevelFolder,
FileBackupFileType.Snapshot::class,
FileBackupFileType.Blob::class,
) { fileInfo ->
when (fileInfo.fileHandle) {
is FileBackupFileType.Snapshot -> {
val handle = fileInfo.fileHandle as FileBackupFileType.Snapshot
val storedSnapshot = StoredSnapshot(handle.topLevelFolder.name, handle.time)
storedSnapshots.add(storedSnapshot)
}
is FileBackupFileType.Blob ->
availableChunkIds[fileInfo.fileHandle.name] = fileInfo.size
else -> error("Unexpected FileHandle: $fileInfo")
}
}
// ensure our local ChunksCache is up to date
if (!db.getChunksCache().areAllAvailableChunksCached(db, availableChunkIds.keys)) {
Log.i(TAG, "Not all available chunks cached, rebuild local cache...")
cacheRepopulater.repopulate(streamKey, availableChunkIds)
}
// parse snapshots
val snapshots = storedSnapshots.map { snapshotRetriever.getSnapshot(streamKey, it) }
Log.i(TAG, "Found ${storedSnapshots.size} snapshots, ${snapshots.size} readable.")
// get all referenced chunkIds
val referencedChunkIds = mutableSetOf<String>()
snapshots.forEach { snapshot ->
snapshot.mediaFilesList.forEach { referencedChunkIds.addAll(it.chunkIdsList) }
snapshot.documentFilesList.forEach { referencedChunkIds.addAll(it.chunkIdsList) }
}
// calculate chunks that are missing
val missingChunkIds = referencedChunkIds - availableChunkIds.keys
Log.i(
TAG, "Found ${referencedChunkIds.size} referenced chunks, " +
"${missingChunkIds.size} missing."
)

val chunkIdMac = mac // keep a copy of the mac
checkBlobSample(referencedChunkIds, percent).forEach { chunkId ->
val handle = FileBackupFileType.Blob(androidId, chunkId)
val cachedChunk = db.getChunksCache().get(chunkId)
// if chunk is not in DB, it isn't available on backend,
// so missing version doesn't matter
val version = cachedChunk?.version ?: VERSION
val readId = backend.load(handle).use { inputStream ->
inputStream.readVersion(version.toInt())
val ad = streamCrypto.getAssociatedDataForChunk(chunkId, version)
streamCrypto.newDecryptingStream(streamKey, inputStream, ad)
.use { decryptedStream ->
chunkIdMac.doFinal(decryptedStream.readAllBytes()).toHexString()
}
}
if (readId != chunkId) {
Log.w(TAG, "Wrong chunkId $readId for $chunkId")
} else {
Log.i(TAG, "Checked chunkId $chunkId")
}
}

}

private fun checkBlobSample(referencedChunkIds: Set<String>, percent: Int): List<String> {
val size = getBackupSize()
val targetSize = (size * (percent.toDouble() / 100)).roundToLong()
val blobSample = mutableListOf<String>()
val iterator = referencedChunkIds.shuffled().iterator()
var currentSize = 0L
while (currentSize < targetSize && iterator.hasNext()) {
val chunkId = iterator.next()
blobSample.add(chunkId)
// we ensure cache consistency above, so chunks not in cache don't exist anymore
currentSize += db.getChunksCache().get(chunkId)?.size ?: 0L
}
return blobSample
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ internal interface ChunksCache {
@Query("SELECT COUNT(id) FROM CachedChunk WHERE id IN (:ids)")
fun getNumberOfCachedChunks(ids: Collection<String>): Int

@Query("SELECT SUM(size) FROM CachedChunk WHERE ref_count > 0")
fun getSizeOfCachedChunks(): Long

@Query("SELECT * FROM CachedChunk WHERE ref_count <= 0")
fun getUnreferencedChunks(): List<CachedChunk>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
package org.calyxos.backup.storage.prune

import android.util.Log
import org.calyxos.backup.storage.SnapshotRetriever
import org.calyxos.backup.storage.api.BackupObserver
import org.calyxos.backup.storage.api.StoredSnapshot
import org.calyxos.backup.storage.crypto.StreamCrypto
import org.calyxos.backup.storage.db.Db
import org.calyxos.backup.storage.measure
import org.calyxos.backup.storage.SnapshotRetriever
import org.calyxos.backup.storage.getCurrentBackupSnapshots
import org.calyxos.backup.storage.measure
import org.calyxos.seedvault.core.backends.Backend
import org.calyxos.seedvault.core.backends.FileBackupFileType
import org.calyxos.seedvault.core.crypto.KeyManager
Expand All @@ -24,14 +24,14 @@ private val TAG = Pruner::class.java.simpleName
internal class Pruner(
private val db: Db,
private val retentionManager: RetentionManager,
private val storagePluginGetter: () -> Backend,
private val backendGetter: () -> Backend,
private val androidId: String,
keyManager: KeyManager,
private val snapshotRetriever: SnapshotRetriever,
streamCrypto: StreamCrypto = StreamCrypto,
) {

private val backend get() = storagePluginGetter()
private val backend get() = backendGetter()
private val chunksCache = db.getChunksCache()
private val streamKey = try {
streamCrypto.deriveStreamKey(keyManager.getMainKey())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ internal class PrunerTest {
private val pruner = Pruner(
db = db,
retentionManager = retentionManager,
storagePluginGetter = backendGetter,
backendGetter = backendGetter,
androidId = androidId,
keyManager = keyManager,
snapshotRetriever = snapshotRetriever,
Expand Down

0 comments on commit 09adff7

Please sign in to comment.