Do not generate archive thumbnails for quotes.

This commit is contained in:
Greyson Parrelli
2025-08-26 16:54:04 -04:00
parent d4c1c39179
commit c29d77d4a5
7 changed files with 65 additions and 11 deletions

View File

@@ -51,6 +51,30 @@ class BackupMediaSnapshotTableTest {
assertThat(count).isEqualTo(countWithThumbnails)
}
@Test
fun givenAnEmptyTable_whenIWriteToTableAndCommitQuotes_thenIExpectFilledTableWithNoThumbnails() {
val inputCount = 100
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = inputCount, quote = true))
SignalDatabase.backupMediaSnapshots.commitPendingRows()
val count = getCountForLatestSnapshot(includeThumbnails = true)
assertThat(count).isEqualTo(inputCount)
}
@Test
fun givenAnEmptyTable_whenIWriteToTableAndCommitNonMedia_thenIExpectFilledTableWithNoThumbnails() {
val inputCount = 100
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = inputCount, contentType = "text/plain"))
SignalDatabase.backupMediaSnapshots.commitPendingRows()
val count = getCountForLatestSnapshot(includeThumbnails = true)
assertThat(count).isEqualTo(inputCount)
}
@Test
fun givenAFilledTable_whenIReinsertObjects_thenIExpectUncommittedOverrides() {
val initialCount = 100
@@ -290,19 +314,21 @@ class BackupMediaSnapshotTableTest {
.readToSingleInt(0)
}
private fun generateArchiveMediaItemSequence(count: Int): Sequence<ArchiveMediaItem> {
private fun generateArchiveMediaItemSequence(count: Int, quote: Boolean = false, contentType: String = "image/jpeg"): Sequence<ArchiveMediaItem> {
return (1..count)
.asSequence()
.map { createArchiveMediaItem(it) }
.map { createArchiveMediaItem(it, quote = quote, contentType = contentType) }
}
private fun createArchiveMediaItem(seed: Int, cdn: Int = 0): ArchiveMediaItem {
private fun createArchiveMediaItem(seed: Int, cdn: Int = 0, quote: Boolean = false, contentType: String = "image/jpeg"): ArchiveMediaItem {
return ArchiveMediaItem(
mediaId = "media_id_$seed",
thumbnailMediaId = "thumbnail_media_id_$seed",
cdn = cdn,
plaintextHash = Util.toByteArray(seed),
remoteKey = Util.toByteArray(seed)
remoteKey = Util.toByteArray(seed),
quote = quote,
contentType = contentType
)
}

View File

@@ -41,8 +41,10 @@ import org.signal.core.util.getForeignKeyViolations
import org.signal.core.util.logging.Log
import org.signal.core.util.logging.logW
import org.signal.core.util.money.FiatMoney
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireIntOrNull
import org.signal.core.util.requireNonNullString
import org.signal.core.util.requireString
import org.signal.core.util.stream.NonClosingOutputStream
import org.signal.core.util.urlEncode
import org.signal.core.util.withinTransaction
@@ -2393,11 +2395,22 @@ class ArchiveMediaItemIterator(private val cursor: Cursor) : Iterator<ArchiveMed
val plaintextHash = cursor.requireNonNullString(AttachmentTable.DATA_HASH_END).decodeBase64OrThrow()
val remoteKey = cursor.requireNonNullString(AttachmentTable.REMOTE_KEY).decodeBase64OrThrow()
val cdn = cursor.requireIntOrNull(AttachmentTable.ARCHIVE_CDN)
val quote = cursor.requireBoolean(AttachmentTable.QUOTE)
val contentType = cursor.requireString(AttachmentTable.CONTENT_TYPE)
val mediaId = MediaName.fromPlaintextHashAndRemoteKey(plaintextHash, remoteKey).toMediaId(SignalStore.backup.mediaRootBackupKey).encode()
val thumbnailMediaId = MediaName.fromPlaintextHashAndRemoteKeyForThumbnail(plaintextHash, remoteKey).toMediaId(SignalStore.backup.mediaRootBackupKey).encode()
cursor.moveToNext()
return ArchiveMediaItem(mediaId, thumbnailMediaId, cdn, plaintextHash, remoteKey)
return ArchiveMediaItem(
mediaId = mediaId,
thumbnailMediaId = thumbnailMediaId,
cdn = cdn,
plaintextHash = plaintextHash,
remoteKey = remoteKey,
quote = quote,
contentType = contentType
)
}
}

View File

@@ -416,7 +416,7 @@ class AttachmentTable(
*/
fun getAttachmentsEligibleForArchiveUpload(): Cursor {
return readableDatabase
.select(DATA_HASH_END, REMOTE_KEY, ARCHIVE_CDN)
.select(DATA_HASH_END, REMOTE_KEY, ARCHIVE_CDN, QUOTE, CONTENT_TYPE)
.from(TABLE_NAME)
.where("$DATA_HASH_END NOT NULL AND $REMOTE_KEY NOT NULL AND $ARCHIVE_TRANSFER_STATE != ${ArchiveTransferState.PERMANENT_FAILURE.value}")
.run()

View File

@@ -24,6 +24,7 @@ import org.signal.core.util.toInt
import org.signal.core.util.update
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.backup.v2.ArchivedMediaObject
import org.thoughtcrime.securesms.util.MediaUtil
/**
* When we delete attachments locally, we can't immediately delete them from the archive CDN. This is because there is still a backup that exists that
@@ -140,7 +141,10 @@ class BackupMediaSnapshotTable(context: Context, database: SignalDatabase) : Dat
)
writePendingMediaObjectsChunk(
chunk.map { MediaEntry(it.thumbnailMediaId, it.cdn, it.plaintextHash, it.remoteKey, isThumbnail = true) }
chunk
.filterNot { it.quote }
.filter { MediaUtil.isImageOrVideoType(it.contentType) }
.map { MediaEntry(it.thumbnailMediaId, it.cdn, it.plaintextHash, it.remoteKey, isThumbnail = true) }
)
}
}
@@ -291,6 +295,10 @@ class BackupMediaSnapshotTable(context: Context, database: SignalDatabase) : Dat
}
private fun writePendingMediaObjectsChunk(chunk: List<MediaEntry>) {
if (chunk.isEmpty()) {
return
}
val values = chunk.map {
contentValuesOf(
MEDIA_ID to it.mediaId,
@@ -324,7 +332,9 @@ class BackupMediaSnapshotTable(context: Context, database: SignalDatabase) : Dat
val thumbnailMediaId: String,
val cdn: Int?,
val plaintextHash: ByteArray,
val remoteKey: ByteArray
val remoteKey: ByteArray,
val quote: Boolean,
val contentType: String?
)
class CdnMismatchResult(

View File

@@ -98,6 +98,11 @@ class ArchiveThumbnailUploadJob private constructor(
return Result.success()
}
if (attachment.quote) {
Log.w(TAG, "$attachmentId is a quote, skipping.")
return Result.success()
}
if (attachment.remoteDigest == null && attachment.dataHash == null && attachment.hadIntegrityCheckPerformed()) {
Log.w(TAG, "$attachmentId has no integrity check! Cannot proceed.")
return Result.success()

View File

@@ -366,7 +366,7 @@ class BackupMessagesJob private constructor(
cancellationSignal = { this.isCanceled },
currentTime = currentTime
) {
writeMediaCursorToTemporaryTable(it, currentTime = currentTime, mediaBackupEnabled = SignalStore.backup.backsUpMedia)
writeMediaCursorToTemporaryTable(it, mediaBackupEnabled = SignalStore.backup.backsUpMedia)
}
if (isCanceled) {
@@ -415,7 +415,7 @@ class BackupMessagesJob private constructor(
)
}
private fun writeMediaCursorToTemporaryTable(db: SignalDatabase, mediaBackupEnabled: Boolean, currentTime: Long) {
private fun writeMediaCursorToTemporaryTable(db: SignalDatabase, mediaBackupEnabled: Boolean) {
if (mediaBackupEnabled) {
db.attachmentTable.getAttachmentsEligibleForArchiveUpload().use {
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(

View File

@@ -192,7 +192,7 @@ class CopyAttachmentToArchiveJob private constructor(private val attachmentId: A
Log.d(TAG, "[$attachmentId] Updating archive transfer state to ${AttachmentTable.ArchiveTransferState.FINISHED}")
SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.FINISHED)
if (!isCanceled) {
if (!isCanceled && !attachment.quote) {
ArchiveThumbnailUploadJob.enqueueIfNecessary(attachmentId)
} else {
Log.d(TAG, "[$attachmentId] Refusing to enqueue thumb for canceled upload.")