Update local backup v2 support.

This commit is contained in:
Cody Henthorne
2025-12-18 16:33:30 -05:00
committed by jeffrey-signal
parent 71b15d269e
commit d9ecab5240
55 changed files with 2291 additions and 274 deletions

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database
import android.content.Context
import android.database.Cursor
import org.signal.core.util.delete
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
import org.signal.core.util.readToList
import org.signal.core.util.readToSingleLongOrNull
import org.signal.core.util.requireBlob
import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.attachments.AttachmentMetadata
import org.thoughtcrime.securesms.attachments.LocalBackupKey
import org.thoughtcrime.securesms.util.Util
/**
* Metadata for various attachments. There is a many-to-one relationship with the Attachment table as this metadata
* represents data about a specific data file (plaintext hash).
*/
class AttachmentMetadataTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) {
companion object {
private val TAG = Log.tag(AttachmentMetadataTable::class)
const val TABLE_NAME = "attachment_metadata"
const val ID = "_id"
const val PLAINTEXT_HASH = "plaintext_hash"
const val LOCAL_BACKUP_KEY = "local_backup_key"
const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$PLAINTEXT_HASH TEXT NOT NULL,
$LOCAL_BACKUP_KEY BLOB DEFAULT NULL,
UNIQUE ($PLAINTEXT_HASH)
)
"""
val PROJECTION = arrayOf(LOCAL_BACKUP_KEY)
/**
* Attempts to load metadata from the cursor if present. Returns null iff the cursor contained no
* metadata columns (i.e., no join in the original query). If there are columns, but they are null, the contents of the
* returned [AttachmentMetadata] will be null.
*/
fun getMetadata(cursor: Cursor, localBackupKeyColumn: String = LOCAL_BACKUP_KEY): AttachmentMetadata? {
if (cursor.getColumnIndex(localBackupKeyColumn) >= 0) {
val localBackupKey = cursor.requireBlob(localBackupKeyColumn)?.let { LocalBackupKey(it) }
return AttachmentMetadata(localBackupKey)
}
return null
}
}
fun insert(plaintextHash: String, localBackupKey: ByteArray): Long {
val rowId = writableDatabase
.insertInto(TABLE_NAME)
.values(PLAINTEXT_HASH to plaintextHash, LOCAL_BACKUP_KEY to localBackupKey)
.run(conflictStrategy = SQLiteDatabase.CONFLICT_IGNORE)
if (rowId > 0) {
return rowId
}
return readableDatabase
.select(ID)
.from(TABLE_NAME)
.where("$PLAINTEXT_HASH = ?", plaintextHash)
.run()
.readToSingleLongOrNull()!!
}
fun cleanup() {
writableDatabase
.delete(TABLE_NAME)
.where("$ID NOT IN (SELECT DISTINCT ${AttachmentTable.METADATA_ID} FROM ${AttachmentTable.TABLE_NAME})")
.run()
}
fun insertNewKeysForExistingAttachments() {
writableDatabase.withinTransaction {
do {
val hashes: List<String> = readableDatabase
.select("DISTINCT ${AttachmentTable.DATA_HASH_END}")
.from(AttachmentTable.TABLE_NAME)
.where("${AttachmentTable.DATA_HASH_END} IS NOT NULL AND ${AttachmentTable.DATA_FILE} IS NOT NULL AND ${AttachmentTable.METADATA_ID} IS NULL")
.limit(1000)
.run()
.readToList { it.requireNonNullString(AttachmentTable.DATA_HASH_END) }
if (hashes.isNotEmpty()) {
val newKeys: List<Pair<String, ByteArray>> = hashes.zip(hashes.map { Util.getSecretBytes(64) })
newKeys.forEach { (hash, key) ->
var rowId = writableDatabase
.insertInto(TABLE_NAME)
.values(PLAINTEXT_HASH to hash, LOCAL_BACKUP_KEY to key)
.run(conflictStrategy = SQLiteDatabase.CONFLICT_IGNORE)
if (rowId == -1L) {
rowId = readableDatabase
.select(ID)
.from(TABLE_NAME)
.where("$PLAINTEXT_HASH = ?", hash)
.run()
.readToSingleLongOrNull()!!
}
writableDatabase
.update(AttachmentTable.TABLE_NAME)
.values(AttachmentTable.METADATA_ID to rowId)
.where("${AttachmentTable.DATA_HASH_END} = ?", hash)
.run()
}
}
} while (hashes.isNotEmpty())
}
}
}

View File

@@ -60,6 +60,7 @@ import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireIntOrNull
import org.signal.core.util.requireLong
import org.signal.core.util.requireLongOrNull
import org.signal.core.util.requireNonNullBlob
import org.signal.core.util.requireNonNullString
import org.signal.core.util.requireObject
@@ -73,6 +74,7 @@ import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.attachments.LocalBackupKey
import org.thoughtcrime.securesms.attachments.LocalStickerAttachment
import org.thoughtcrime.securesms.attachments.WallpaperAttachment
import org.thoughtcrime.securesms.audio.AudioHash
@@ -191,6 +193,7 @@ class AttachmentTable(
const val ATTACHMENT_UUID = "attachment_uuid"
const val OFFLOAD_RESTORED_AT = "offload_restored_at"
const val QUOTE_TARGET_CONTENT_TYPE = "quote_target_content_type"
const val METADATA_ID = "metadata_id"
const val ATTACHMENT_JSON_ALIAS = "attachment_json"
@@ -249,6 +252,8 @@ class AttachmentTable(
ATTACHMENT_UUID
)
private val PROJECTION_WITH_METADATA = PROJECTION.map { if (it == ID) "$TABLE_NAME.$ID" else it }.toTypedArray() + AttachmentMetadataTable.PROJECTION
@JvmField
val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
@@ -293,7 +298,8 @@ class AttachmentTable(
$ATTACHMENT_UUID TEXT DEFAULT NULL,
$OFFLOAD_RESTORED_AT INTEGER DEFAULT 0,
$QUOTE_TARGET_CONTENT_TYPE TEXT DEFAULT NULL,
$ARCHIVE_THUMBNAIL_TRANSFER_STATE INTEGER DEFAULT ${ArchiveTransferState.NONE.value}
$ARCHIVE_THUMBNAIL_TRANSFER_STATE INTEGER DEFAULT ${ArchiveTransferState.NONE.value},
$METADATA_ID INTEGER DEFAULT NULL REFERENCES ${AttachmentMetadataTable.TABLE_NAME} (${AttachmentMetadataTable.ID})
)
"""
@@ -309,11 +315,12 @@ class AttachmentTable(
"CREATE INDEX IF NOT EXISTS $DATA_HASH_REMOTE_KEY_INDEX ON $TABLE_NAME ($DATA_HASH_END, $REMOTE_KEY);",
"CREATE INDEX IF NOT EXISTS $DATA_FILE_INDEX ON $TABLE_NAME ($DATA_FILE);",
"CREATE INDEX IF NOT EXISTS attachment_archive_transfer_state ON $TABLE_NAME ($ARCHIVE_TRANSFER_STATE);",
"CREATE INDEX IF NOT EXISTS attachment_remote_digest_index ON $TABLE_NAME ($REMOTE_DIGEST);"
"CREATE INDEX IF NOT EXISTS attachment_remote_digest_index ON $TABLE_NAME ($REMOTE_DIGEST);",
"CREATE INDEX IF NOT EXISTS attachment_metadata_id ON $TABLE_NAME ($METADATA_ID);"
)
private val DATA_FILE_INFO_PROJECTION = arrayOf(
ID, DATA_FILE, DATA_SIZE, DATA_RANDOM, DATA_HASH_START, DATA_HASH_END, TRANSFORM_PROPERTIES, UPLOAD_TIMESTAMP, ARCHIVE_CDN, ARCHIVE_TRANSFER_STATE, THUMBNAIL_FILE, THUMBNAIL_RESTORE_STATE, THUMBNAIL_RANDOM
ID, DATA_FILE, DATA_SIZE, DATA_RANDOM, DATA_HASH_START, DATA_HASH_END, TRANSFORM_PROPERTIES, UPLOAD_TIMESTAMP, ARCHIVE_CDN, ARCHIVE_TRANSFER_STATE, THUMBNAIL_FILE, THUMBNAIL_RESTORE_STATE, THUMBNAIL_RANDOM, METADATA_ID
)
private const val QUOTE_THUMBNAIL_DIMEN = 200
@@ -325,6 +332,8 @@ class AttachmentTable(
/** Indicates a quote from a free-tier backup restore is pending potential reconstruction from a parent attachment. */
const val QUOTE_PENDING_RECONSTRUCTION = 3
private val TABLE_NAME_WITH_METADTA = "$TABLE_NAME LEFT JOIN ${AttachmentMetadataTable.TABLE_NAME} ON $TABLE_NAME.$METADATA_ID = ${AttachmentMetadataTable.TABLE_NAME}.${AttachmentMetadataTable.ID}"
@JvmStatic
@Throws(IOException::class)
fun newDataFile(context: Context): File {
@@ -472,13 +481,15 @@ class AttachmentTable(
.firstOrNull()
}
fun getAttachmentIdByPlaintextHashAndRemoteKey(plaintextHash: ByteArray, remoteKey: ByteArray): AttachmentId? {
fun getAttachmentWithMetadata(attachmentId: AttachmentId): DatabaseAttachment? {
return readableDatabase
.select(ID)
.from("$TABLE_NAME INDEXED BY $DATA_HASH_REMOTE_KEY_INDEX")
.where("$DATA_HASH_END = ? AND $REMOTE_KEY = ?", Base64.encodeWithPadding(plaintextHash), Base64.encodeWithPadding(remoteKey))
.select(*PROJECTION_WITH_METADATA)
.from(TABLE_NAME_WITH_METADTA)
.where("$TABLE_NAME.$ID = ?", attachmentId.id)
.run()
.readToSingleObject { AttachmentId(it.requireLong(ID)) }
.readToList { it.readAttachments() }
.flatten()
.firstOrNull()
}
fun getAttachmentsForMessage(mmsId: Long): List<DatabaseAttachment> {
@@ -492,23 +503,37 @@ class AttachmentTable(
.flatten()
}
@JvmOverloads
fun getAttachmentsForMessages(mmsIds: Collection<Long?>, excludeTranscodingQuotes: Boolean = false): Map<Long, List<DatabaseAttachment>> {
fun getAttachmentsForMessagesArchive(mmsIds: Collection<Long>): Map<Long, List<DatabaseAttachment>> {
if (mmsIds.isEmpty()) {
return emptyMap()
}
val query = SqlUtil.buildFastCollectionQuery(MESSAGE_ID, mmsIds)
val where = if (excludeTranscodingQuotes) {
"(${query.where}) AND $QUOTE != $QUOTE_PENDING_TRANSCODE"
} else {
query.where
val where = "(${query.where}) AND $QUOTE != $QUOTE_PENDING_TRANSCODE"
return readableDatabase
.select(*PROJECTION_WITH_METADATA)
.from(TABLE_NAME_WITH_METADTA)
.where(where, query.whereArgs)
.orderBy("$TABLE_NAME.$ID ASC")
.run()
.groupBy { cursor ->
val attachment = cursor.readAttachment()
attachment.mmsId to attachment
}
}
fun getAttachmentsForMessages(mmsIds: Collection<Long?>): Map<Long, List<DatabaseAttachment>> {
if (mmsIds.isEmpty()) {
return emptyMap()
}
val query = SqlUtil.buildFastCollectionQuery(MESSAGE_ID, mmsIds)
return readableDatabase
.select(*PROJECTION)
.from(TABLE_NAME)
.where(where, query.whereArgs)
.where(query.where, query.whereArgs)
.orderBy("$ID ASC")
.run()
.groupBy { cursor ->
@@ -559,38 +584,20 @@ class AttachmentTable(
.flatten()
}
fun getLocalArchivableAttachment(plaintextHash: String, remoteKey: String): LocalArchivableAttachment? {
return readableDatabase
.select(*PROJECTION)
.from(TABLE_NAME)
.where("$DATA_HASH_END = ? AND $REMOTE_KEY = ?")
.orderBy("$ID DESC")
.limit(1)
.run()
.readToSingleObject {
LocalArchivableAttachment(
file = File(it.requireNonNullString(DATA_FILE)),
random = it.requireNonNullBlob(DATA_RANDOM),
size = it.requireLong(DATA_SIZE),
remoteKey = Base64.decode(it.requireNonNullString(REMOTE_KEY)),
plaintextHash = Base64.decode(it.requireNonNullString(DATA_HASH_END))
)
}
}
fun getLocalArchivableAttachments(): List<LocalArchivableAttachment> {
return readableDatabase
.select(*PROJECTION)
.from(TABLE_NAME)
.where("$REMOTE_KEY IS NOT NULL AND $DATA_HASH_END IS NOT NULL AND $DATA_FILE IS NOT NULL")
.orderBy("$ID DESC")
.select(*PROJECTION_WITH_METADATA)
.from(TABLE_NAME_WITH_METADTA)
.where("$DATA_HASH_END IS NOT NULL AND $DATA_FILE IS NOT NULL AND ${AttachmentMetadataTable.TABLE_NAME}.${AttachmentMetadataTable.LOCAL_BACKUP_KEY} IS NOT NULL")
.orderBy("$TABLE_NAME.$ID DESC")
.run()
.readToList {
LocalArchivableAttachment(
attachmentId = AttachmentId(it.requireLong(ID)),
file = File(it.requireNonNullString(DATA_FILE)),
random = it.requireNonNullBlob(DATA_RANDOM),
size = it.requireLong(DATA_SIZE),
remoteKey = Base64.decode(it.requireNonNullString(REMOTE_KEY)),
localBackupKey = AttachmentMetadataTable.getMetadata(it)!!.localBackupKey!!,
plaintextHash = Base64.decode(it.requireNonNullString(DATA_HASH_END))
)
}
@@ -646,26 +653,6 @@ class AttachmentTable(
}
}
fun getRestorableAttachments(batchSize: Int): List<RestorableAttachment> {
return readableDatabase
.select(ID, MESSAGE_ID, DATA_SIZE, DATA_HASH_END, REMOTE_KEY, STICKER_PACK_ID)
.from(TABLE_NAME)
.where("$TRANSFER_STATE = ?", TRANSFER_NEEDS_RESTORE)
.limit(batchSize)
.orderBy("$ID DESC")
.run()
.readToList {
RestorableAttachment(
attachmentId = AttachmentId(it.requireLong(ID)),
mmsId = it.requireLong(MESSAGE_ID),
size = it.requireLong(DATA_SIZE),
plaintextHash = it.requireString(DATA_HASH_END)?.let { hash -> Base64.decode(hash) },
remoteKey = it.requireString(REMOTE_KEY)?.let { key -> Base64.decode(key) },
stickerPackId = it.requireString(STICKER_PACK_ID)
)
}
}
fun getRestorableOptimizedAttachments(): List<RestorableAttachment> {
return readableDatabase
.select(ID, MESSAGE_ID, DATA_SIZE, DATA_HASH_END, REMOTE_KEY, STICKER_PACK_ID)
@@ -685,6 +672,25 @@ class AttachmentTable(
}
}
fun getRestorableLocalAttachments(batchSize: Int): List<LocalRestorableAttachment> {
return readableDatabase
.select("$TABLE_NAME.$ID", MESSAGE_ID, DATA_HASH_END, STICKER_PACK_ID, AttachmentMetadataTable.LOCAL_BACKUP_KEY)
.from(TABLE_NAME_WITH_METADTA)
.where("$TRANSFER_STATE = ?", TRANSFER_NEEDS_RESTORE)
.limit(batchSize)
.orderBy("$TABLE_NAME.$ID DESC")
.run()
.readToList {
LocalRestorableAttachment(
attachmentId = AttachmentId(it.requireLong(ID)),
mmsId = it.requireLong(MESSAGE_ID),
plaintextHash = it.requireString(DATA_HASH_END)?.let { hash -> Base64.decode(hash) },
localBackupKey = it.requireBlob(AttachmentMetadataTable.LOCAL_BACKUP_KEY)?.let { key -> LocalBackupKey(key) },
stickerPackId = it.requireString(STICKER_PACK_ID)
)
}
}
fun getRemainingRestorableAttachmentSize(): Long {
return readableDatabase
.rawQuery(
@@ -1239,6 +1245,8 @@ class AttachmentTable(
.where("$MESSAGE_ID = ?", mmsId)
.run()
SignalDatabase.attachmentMetadata.cleanup()
AppDependencies.databaseObserver.notifyAttachmentDeletedObservers()
deleteCount
@@ -1315,11 +1323,14 @@ class AttachmentTable(
TRANSFER_STATE to TRANSFER_PROGRESS_DONE,
ARCHIVE_TRANSFER_STATE to ArchiveTransferState.NONE.value,
BLUR_HASH to null,
CONTENT_TYPE to MediaUtil.VIEW_ONCE
CONTENT_TYPE to MediaUtil.VIEW_ONCE,
METADATA_ID to null
)
.where("$MESSAGE_ID = ?", messageId)
.run()
SignalDatabase.attachmentMetadata.cleanup()
AppDependencies.databaseObserver.notifyAttachmentDeletedObservers()
val threadId = messages.getThreadIdForMessage(messageId)
@@ -1357,6 +1368,8 @@ class AttachmentTable(
.where("$ID = ?", id.id)
.run()
SignalDatabase.attachmentMetadata.cleanup()
if (filePath != null && isSafeToDeleteDataFile(filePath, id)) {
filePathsToDelete += filePath
contentType?.let { contentTypesToDelete += it }
@@ -1490,6 +1503,7 @@ class AttachmentTable(
Log.d(TAG, "[deleteAllAttachments]")
writableDatabase.deleteAll(TABLE_NAME)
SignalDatabase.attachmentMetadata.cleanup()
FileUtils.deleteDirectoryContents(context.getDir(DIRECTORY, Context.MODE_PRIVATE))
@@ -1647,6 +1661,7 @@ class AttachmentTable(
values.put(DATA_RANDOM, hashMatch.random)
values.put(DATA_HASH_START, hashMatch.hashEnd)
values.put(DATA_HASH_END, hashMatch.hashEnd)
values.put(METADATA_ID, hashMatch.metadataId)
if (archiveRestore) {
// We aren't getting an updated remote key/mediaName when restoring, can reuse
values.put(ARCHIVE_CDN, hashMatch.archiveCdn)
@@ -2295,7 +2310,8 @@ class AttachmentTable(
archiveCdn = if (jsonObject.isNull(ARCHIVE_CDN)) null else jsonObject.getInt(ARCHIVE_CDN),
thumbnailRestoreState = ThumbnailRestoreState.deserialize(jsonObject.getInt(THUMBNAIL_RESTORE_STATE)),
archiveTransferState = ArchiveTransferState.deserialize(jsonObject.getInt(ARCHIVE_TRANSFER_STATE)),
uuid = UuidUtil.parseOrNull(jsonObject.getString(ATTACHMENT_UUID))
uuid = UuidUtil.parseOrNull(jsonObject.getString(ATTACHMENT_UUID)),
metadata = null
)
}
}
@@ -2764,6 +2780,12 @@ class AttachmentTable(
val attachmentId: AttachmentId = writableDatabase.withinTransaction { db ->
val plaintextHash = attachment.plaintextHash.takeIf { it.isNotEmpty() }?.let { Base64.encodeWithPadding(it) }
val metadataId: Long? = if (plaintextHash != null && attachment.localBackupKey != null) {
SignalDatabase.attachmentMetadata.insert(plaintextHash, attachment.localBackupKey)
} else {
null
}
val contentValues = ContentValues().apply {
put(MESSAGE_ID, messageId)
put(CONTENT_TYPE, attachment.contentType)
@@ -2808,6 +2830,10 @@ class AttachmentTable(
} else {
putNull(REMOTE_INCREMENTAL_DIGEST)
}
if (metadataId != null && metadataId > 0) {
put(METADATA_ID, metadataId)
}
}
val rowId = db.insert(TABLE_NAME, null, contentValues)
@@ -2870,6 +2896,7 @@ class AttachmentTable(
put(THUMBNAIL_RANDOM, dataFileInfo.thumbnailRandom)
put(THUMBNAIL_FILE, dataFileInfo.thumbnailFile)
put(ATTACHMENT_UUID, stickerAttachment.uuid?.toString())
put(METADATA_ID, dataFileInfo.metadataId)
}
val rowId = db.insert(TABLE_NAME, null, contentValues)
@@ -2911,7 +2938,7 @@ class AttachmentTable(
attachmentId = AttachmentId(rowId)
}
return attachmentId as AttachmentId
return attachmentId
}
/**
@@ -2994,6 +3021,7 @@ class AttachmentTable(
contentValues.put(DATA_RANDOM, hashMatch.random)
contentValues.put(DATA_HASH_START, fileWriteResult.hash)
contentValues.put(DATA_HASH_END, hashMatch.hashEnd)
contentValues.put(METADATA_ID, hashMatch.metadataId)
if (hashMatch.transformProperties.skipTransform) {
Log.i(TAG, "[insertAttachmentWithData] The hash match has a DATA_HASH_END and skipTransform=true, so skipping transform of the new file as well. (MessageId: $messageId, ${attachment.uri})")
@@ -3249,7 +3277,8 @@ class AttachmentTable(
archiveCdn = cursor.requireIntOrNull(ARCHIVE_CDN),
thumbnailRestoreState = ThumbnailRestoreState.deserialize(cursor.requireInt(THUMBNAIL_RESTORE_STATE)),
archiveTransferState = ArchiveTransferState.deserialize(cursor.requireInt(ARCHIVE_TRANSFER_STATE)),
uuid = UuidUtil.parseOrNull(cursor.requireString(ATTACHMENT_UUID))
uuid = UuidUtil.parseOrNull(cursor.requireString(ATTACHMENT_UUID)),
metadata = AttachmentMetadataTable.getMetadata(cursor)
)
}
@@ -3278,7 +3307,8 @@ class AttachmentTable(
archiveTransferState = this.requireInt(ARCHIVE_TRANSFER_STATE),
thumbnailFile = this.requireString(THUMBNAIL_FILE),
thumbnailRandom = this.requireBlob(THUMBNAIL_RANDOM),
thumbnailRestoreState = this.requireInt(THUMBNAIL_RESTORE_STATE)
thumbnailRestoreState = this.requireInt(THUMBNAIL_RESTORE_STATE),
metadataId = this.requireLongOrNull(METADATA_ID)
)
}
@@ -3649,7 +3679,8 @@ class AttachmentTable(
val archiveTransferState: Int,
val thumbnailFile: String?,
val thumbnailRandom: ByteArray?,
val thumbnailRestoreState: Int
val thumbnailRestoreState: Int,
val metadataId: Long?
)
@VisibleForTesting
@@ -3840,11 +3871,12 @@ class AttachmentTable(
class SyncAttachment(val id: AttachmentId, val uuid: UUID?, val digest: ByteArray?, val plaintextHash: String?)
class LocalArchivableAttachment(
val attachmentId: AttachmentId,
val file: File,
val random: ByteArray,
val size: Long,
val plaintextHash: ByteArray,
val remoteKey: ByteArray
val localBackupKey: LocalBackupKey
)
data class RestorableAttachment(
@@ -3864,6 +3896,22 @@ class AttachmentTable(
}
}
data class LocalRestorableAttachment(
val attachmentId: AttachmentId,
val mmsId: Long,
val plaintextHash: ByteArray?,
val localBackupKey: LocalBackupKey?,
val stickerPackId: String?
) {
override fun equals(other: Any?): Boolean {
return this === other || attachmentId == (other as? RestorableAttachment)?.attachmentId
}
override fun hashCode(): Int {
return attachmentId.hashCode()
}
}
data class DebugAttachmentStats(
val totalAttachmentRows: Long = 0L,
val totalUniqueMediaNamesEligibleForUpload: Long = 0L,

View File

@@ -82,6 +82,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
val backupMediaSnapshotTable: BackupMediaSnapshotTable = BackupMediaSnapshotTable(context, this)
val pollTable: PollTables = PollTables(context, this)
val lastResortKeyTuples: LastResortKeyTupleTable = LastResortKeyTupleTable(context, this)
val attachmentMetadataTable: AttachmentMetadataTable = AttachmentMetadataTable(context, this)
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
db.setForeignKeyConstraintsEnabled(true)
@@ -152,6 +153,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
executeStatements(db, PollTables.CREATE_TABLE)
db.execSQL(BackupMediaSnapshotTable.CREATE_TABLE)
db.execSQL(LastResortKeyTupleTable.CREATE_TABLE)
db.execSQL(AttachmentMetadataTable.CREATE_TABLE)
executeStatements(db, RecipientTable.CREATE_INDEXS)
executeStatements(db, MessageTable.CREATE_INDEXS)
@@ -597,5 +599,10 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
@get:JvmName("lastResortKeyTuples")
val lastResortKeyTuples: LastResortKeyTupleTable
get() = instance!!.lastResortKeyTuples
@get:JvmStatic
@get:JvmName("attachmentMetadata")
val attachmentMetadata: AttachmentMetadataTable
get() = instance!!.attachmentMetadataTable
}
}

View File

@@ -152,6 +152,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V295_AddLastRestore
import org.thoughtcrime.securesms.database.helpers.migration.V296_RemovePollVoteConstraint
import org.thoughtcrime.securesms.database.helpers.migration.V297_AddPinnedMessageColumns
import org.thoughtcrime.securesms.database.helpers.migration.V298_DoNotBackupReleaseNotes
import org.thoughtcrime.securesms.database.helpers.migration.V299_AddAttachmentMetadataTable
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
/**
@@ -310,10 +311,11 @@ object SignalDatabaseMigrations {
295 to V295_AddLastRestoreKeyTypeTableIfMissingMigration,
296 to V296_RemovePollVoteConstraint,
297 to V297_AddPinnedMessageColumns,
298 to V298_DoNotBackupReleaseNotes
298 to V298_DoNotBackupReleaseNotes,
299 to V299_AddAttachmentMetadataTable
)
const val DATABASE_VERSION = 298
const val DATABASE_VERSION = 299
@JvmStatic
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import org.thoughtcrime.securesms.database.SQLiteDatabase
/**
* We need to keep track of the local backup key
*/
@Suppress("ClassName")
object V299_AddAttachmentMetadataTable : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL(
"""
CREATE TABLE attachment_metadata (
_id INTEGER PRIMARY KEY,
plaintext_hash TEXT NOT NULL,
local_backup_key BLOB DEFAULT NULL,
UNIQUE (plaintext_hash)
)
"""
)
db.execSQL("ALTER TABLE attachment ADD COLUMN metadata_id INTEGER DEFAULT NULL REFERENCES attachment_metadata (_id)")
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_metadata_id ON attachment (metadata_id)")
}
}