From 230e7f6201d0890919d019556aa54561647300ca Mon Sep 17 00:00:00 2001 From: null Date: Tue, 30 Jun 2026 20:42:38 -0500 Subject: [PATCH] feat(backup): add uploadBackupSnapshot and deleteBackupSnapshot to FirebaseStorageDataSource --- .../data/remote/FirebaseStorageDataSource.kt | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/app/src/main/java/app/closer/data/remote/FirebaseStorageDataSource.kt b/app/src/main/java/app/closer/data/remote/FirebaseStorageDataSource.kt index aaff027b..bd92a8cd 100644 --- a/app/src/main/java/app/closer/data/remote/FirebaseStorageDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirebaseStorageDataSource.kt @@ -68,6 +68,37 @@ class FirebaseStorageDataSource @Inject constructor( .addOnFailureListener { cont.resumeWithException(it) } } + /** + * Uploads an already-encrypted conversation-backup snapshot under the uploader's OWN path and + * returns the tokenized download URL (recorded in the couple-gated backup manifest so the partner + * can fetch it). The bytes are couple-key ciphertext, so Storage holds nothing readable. Living + * under users/{uid}/ means the existing account-delete cleanup covers backups too. + */ + suspend fun uploadBackupSnapshot(uid: String, snapshotId: String, encryptedBytes: ByteArray): String = + suspendCancellableCoroutine { cont -> + if (!isOnline()) { + cont.resumeWithException(IOException("No internet connection")) + return@suspendCancellableCoroutine + } + val ref = storage.reference.child("users/$uid/backups/$snapshotId") + val metadata = StorageMetadata.Builder() + .setContentType("application/octet-stream") + .build() + ref.putBytes(encryptedBytes, metadata) + .continueWithTask { ref.downloadUrl } + .addOnSuccessListener { cont.resume(it.toString()) } + .addOnFailureListener { cont.resumeWithException(it) } + } + + /** Best-effort delete of a previously-uploaded backup snapshot (post-compaction cleanup; succeeds + * only for the uploader's own path, so a cross-partner stale blob is left for that owner/cleanup). */ + suspend fun deleteBackupSnapshot(uid: String, snapshotId: String): Unit = + suspendCancellableCoroutine { cont -> + storage.reference.child("users/$uid/backups/$snapshotId").delete() + .addOnSuccessListener { cont.resume(Unit) } + .addOnFailureListener { cont.resume(Unit) } // best-effort; a leftover blob is harmless ciphertext + } + /** * Downloads the raw (still-encrypted) bytes for a media message over HTTP using the tokenized * download URL, so the partner can read the author's object (the URL token authorizes it,