Remove orphaned attachments when creating a new backup.

This commit is contained in:
Alex Hart
2024-11-22 10:50:42 -04:00
committed by Greyson Parrelli
parent bae86d127f
commit c7f226b5cc
12 changed files with 464 additions and 5 deletions

View File

@@ -407,6 +407,14 @@ class AttachmentTable(
}
}
fun getMediaIdCursor(): Cursor {
return readableDatabase
.select(ARCHIVE_MEDIA_ID, ARCHIVE_CDN)
.from(TABLE_NAME)
.where("$ARCHIVE_MEDIA_ID IS NOT NULL")
.run()
}
fun getAttachment(attachmentId: AttachmentId): DatabaseAttachment? {
return readableDatabase
.select(*PROJECTION)

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database
import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.core.content.contentValuesOf
import org.signal.core.util.SqlUtil
import org.signal.core.util.delete
import org.signal.core.util.exists
import org.signal.core.util.readToList
import org.signal.core.util.requireInt
import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
import org.thoughtcrime.securesms.backup.v2.ArchivedMediaObject
/**
* Helper table for attachment deletion sync
*/
class BackupMediaSnapshotTable(context: Context, database: SignalDatabase) : DatabaseTable(context, database) {
companion object {
const val TABLE_NAME = "backup_media_snapshot"
private const val ID = "_id"
/**
* Generated media id matching that of the attachments table.
*/
private const val MEDIA_ID = "media_id"
/**
* CDN where the data is stored
*/
private const val CDN = "cdn"
/**
* Unique backup snapshot sync time. These are expected to increment in value
* where newer backups have a greater backup id value.
*/
@VisibleForTesting
const val LAST_SYNC_TIME = "last_sync_time"
/**
* Pending sync time, set while a backup is in the process of being exported.
*/
@VisibleForTesting
const val PENDING_SYNC_TIME = "pending_sync_time"
val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$MEDIA_ID TEXT UNIQUE,
$CDN INTEGER,
$LAST_SYNC_TIME INTEGER DEFAULT 0,
$PENDING_SYNC_TIME INTEGER
)
""".trimIndent()
private const val ON_MEDIA_ID_CONFLICT = """
ON CONFLICT($MEDIA_ID) DO UPDATE SET
$PENDING_SYNC_TIME = EXCLUDED.$PENDING_SYNC_TIME,
$CDN = EXCLUDED.$CDN
"""
}
/**
* Creates the temporary table if it doesn't exist, clears it, then inserts the media objects into it.
*/
fun writePendingMediaObjects(mediaObjects: Sequence<ArchivedMediaObject>, pendingSyncTime: Long) {
mediaObjects.chunked(999)
.forEach { chunk ->
writePendingMediaObjectsChunk(chunk, pendingSyncTime)
}
}
private fun writePendingMediaObjectsChunk(chunk: List<ArchivedMediaObject>, pendingSyncTime: Long) {
SqlUtil.buildBulkInsert(
TABLE_NAME,
arrayOf(MEDIA_ID, CDN, PENDING_SYNC_TIME),
chunk.map {
contentValuesOf(MEDIA_ID to it.mediaId, CDN to it.cdn, PENDING_SYNC_TIME to pendingSyncTime)
}
).forEach {
writableDatabase.execSQL("${it.where} $ON_MEDIA_ID_CONFLICT", it.whereArgs)
}
}
/**
* Copies all entries from the temporary table to the persistent table, then deletes the temporary table.
*/
fun commitPendingRows() {
writableDatabase.execSQL("UPDATE $TABLE_NAME SET $LAST_SYNC_TIME = $PENDING_SYNC_TIME")
}
fun getPageOfOldMediaObjects(currentSyncTime: Long, pageSize: Int): List<ArchivedMediaObject> {
return readableDatabase.select(MEDIA_ID, CDN)
.from(TABLE_NAME)
.where("$LAST_SYNC_TIME < ? AND $LAST_SYNC_TIME = $PENDING_SYNC_TIME", currentSyncTime)
.limit(pageSize)
.run()
.readToList {
ArchivedMediaObject(mediaId = it.requireNonNullString(MEDIA_ID), cdn = it.requireInt(CDN))
}
}
fun deleteMediaObjects(mediaObjects: List<ArchivedMediaObject>) {
SqlUtil.buildCollectionQuery(MEDIA_ID, mediaObjects.map { it.mediaId }).forEach {
writableDatabase.delete(TABLE_NAME)
.where(it.where, it.whereArgs)
.run()
}
}
fun hasOldMediaObjects(currentSyncTime: Long): Boolean {
return readableDatabase.exists(TABLE_NAME).where("$LAST_SYNC_TIME > ? AND $LAST_SYNC_TIME = $PENDING_SYNC_TIME", currentSyncTime).run()
}
}

View File

@@ -77,6 +77,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
val inAppPaymentTable: InAppPaymentTable = InAppPaymentTable(context, this)
val inAppPaymentSubscriberTable: InAppPaymentSubscriberTable = InAppPaymentSubscriberTable(context, this)
val chatFoldersTable: ChatFolderTables = ChatFolderTables(context, this)
val backupMediaSnapshotTable: BackupMediaSnapshotTable = BackupMediaSnapshotTable(context, this)
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
db.setForeignKeyConstraintsEnabled(true)
@@ -122,6 +123,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
executeStatements(db, NotificationProfileDatabase.CREATE_TABLE)
executeStatements(db, DistributionListTables.CREATE_TABLE)
executeStatements(db, ChatFolderTables.CREATE_TABLE)
db.execSQL(BackupMediaSnapshotTable.CREATE_TABLE)
executeStatements(db, RecipientTable.CREATE_INDEXS)
executeStatements(db, MessageTable.CREATE_INDEXS)
@@ -566,5 +568,10 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
@get:JvmName("chatFolders")
val chatFolders: ChatFolderTables
get() = instance!!.chatFoldersTable
@get:JvmStatic
@get:JvmName("backupMediaSnapshots")
val backupMediaSnapshots: BackupMediaSnapshotTable
get() = instance!!.backupMediaSnapshotTable
}
}

View File

@@ -113,6 +113,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V253_CreateChatFold
import org.thoughtcrime.securesms.database.helpers.migration.V254_AddChatFolderConstraint
import org.thoughtcrime.securesms.database.helpers.migration.V255_AddCallTableLogIndex
import org.thoughtcrime.securesms.database.helpers.migration.V256_FixIncrementalDigestColumns
import org.thoughtcrime.securesms.database.helpers.migration.V257_CreateBackupMediaSyncTable
/**
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
@@ -228,10 +229,11 @@ object SignalDatabaseMigrations {
253 to V253_CreateChatFolderTables,
254 to V254_AddChatFolderConstraint,
255 to V255_AddCallTableLogIndex,
256 to V256_FixIncrementalDigestColumns
256 to V256_FixIncrementalDigestColumns,
257 to V257_CreateBackupMediaSyncTable
)
const val DATABASE_VERSION = 256
const val DATABASE_VERSION = 257
@JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import net.zetetic.database.sqlcipher.SQLiteDatabase
@Suppress("ClassName")
object V257_CreateBackupMediaSyncTable : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL(
"""
CREATE TABLE backup_media_snapshot (
_id INTEGER PRIMARY KEY,
media_id TEXT UNIQUE,
cdn INTEGER,
last_sync_time INTEGER DEFAULT 0,
pending_sync_time INTEGER
)
""".trimIndent()
)
}
}