mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 18:00:02 +01:00
Remove orphaned attachments when creating a new backup.
This commit is contained in:
committed by
Greyson Parrelli
parent
bae86d127f
commit
c7f226b5cc
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user