Do not upload long text attachments to the archive.

This commit is contained in:
Greyson Parrelli
2025-07-21 11:43:15 -04:00
parent 7295787e08
commit 6af3f2ce42
7 changed files with 57 additions and 43 deletions

View File

@@ -44,7 +44,6 @@ import java.util.UUID
import kotlin.random.Random
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
@@ -286,25 +285,6 @@ class AttachmentTableTest {
assertThat(attachments).isNotEmpty()
}
@Test
fun givenAnAttachmentWithAMessageWithExpirationStartedThatExpiresIn5Minutes_whenIGetAttachmentsThatNeedArchiveUpload_thenIDoNotExpectThatAttachment() {
// GIVEN
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
val blobUncompressed = BlobProvider.getInstance().forData(uncompressData).createForSingleSessionInMemory()
val attachment = createAttachment(1, blobUncompressed, AttachmentTable.TransformProperties.empty())
val message = createIncomingMessage(serverTime = 0.days, attachment = attachment, expiresIn = 5.days)
val messageId = SignalDatabase.messages.insertMessageInbox(message).map { it.messageId }.get()
SignalDatabase.messages.markExpireStarted(messageId, startedTimestamp = System.currentTimeMillis() - (4.days + 12.hours).inWholeMilliseconds)
SignalDatabase.attachments.setArchiveTransferState(AttachmentId(1L), AttachmentTable.ArchiveTransferState.NONE)
SignalDatabase.attachments.setTransferState(messageId, AttachmentId(1L), AttachmentTable.TRANSFER_PROGRESS_DONE)
// WHEN
val attachments = SignalDatabase.attachments.getAttachmentsThatNeedArchiveUpload()
// THEN
assertThat(attachments).isEmpty()
}
@Test
fun givenAnAttachmentWithAMessageWithExpirationStartedThatExpiresIn5Days_whenIGetAttachmentsThatNeedArchiveUpload_thenIDoExpectThatAttachment() {
// GIVEN
@@ -324,6 +304,24 @@ class AttachmentTableTest {
assertThat(attachments).isNotEmpty()
}
@Test
fun givenAnAttachmentWithALongTextAttachment_whenIGetAttachmentsThatNeedArchiveUpload_thenIDoNotExpectThatAttachment() {
// GIVEN
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
val blobUncompressed = BlobProvider.getInstance().forData(uncompressData).createForSingleSessionInMemory()
val attachment = createAttachment(1, blobUncompressed, AttachmentTable.TransformProperties.empty(), contentType = MediaUtil.LONG_TEXT)
val message = createIncomingMessage(serverTime = 0.days, attachment = attachment)
val messageId = SignalDatabase.messages.insertMessageInbox(message).map { it.messageId }.get()
SignalDatabase.attachments.setArchiveTransferState(AttachmentId(1L), AttachmentTable.ArchiveTransferState.NONE)
SignalDatabase.attachments.setTransferState(messageId, AttachmentId(1L), AttachmentTable.TRANSFER_PROGRESS_DONE)
// WHEN
val attachments = SignalDatabase.attachments.getAttachmentsThatNeedArchiveUpload()
// THEN
assertThat(attachments).isEmpty()
}
private fun createIncomingMessage(
serverTime: Duration,
attachment: Attachment,
@@ -395,11 +393,11 @@ class AttachmentTableTest {
)
}
private fun createAttachment(id: Long, uri: Uri, transformProperties: AttachmentTable.TransformProperties): UriAttachment {
private fun createAttachment(id: Long, uri: Uri, transformProperties: AttachmentTable.TransformProperties, contentType: String = MediaUtil.IMAGE_JPEG): UriAttachment {
return UriAttachmentBuilder.build(
id,
uri = uri,
contentType = MediaUtil.IMAGE_JPEG,
contentType = contentType,
transformProperties = transformProperties
)
}

View File

@@ -836,16 +836,20 @@ object BackupRepository {
var frameCount = 0L
writer.use {
val debugInfo = buildDebugInfo()
eventTimer.emit("debug-info")
writer.write(
BackupInfo(
version = VERSION,
backupTimeMs = exportState.backupTime,
mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey.value.toByteString(),
firstAppVersion = SignalStore.backup.firstAppVersion,
debugInfo = buildDebugInfo()
debugInfo = debugInfo
)
)
frameCount++
eventTimer.emit("header")
// We're using a snapshot, so the transaction is more for perf than correctness
dbSnapshot.rawWritableDatabase.withinTransaction {

View File

@@ -630,12 +630,6 @@ class AttachmentTable(
.readToSingleLong()
}
private fun getMessageDoesNotExpireWithinTimeoutClause(): String {
val messageHasExpiration = "${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} > 0"
val messageExpiresInOneDayAfterViewing = "$messageHasExpiration AND ${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} < ${1.days.inWholeMilliseconds}"
return "NOT $messageExpiresInOneDayAfterViewing"
}
/**
* Finds all of the attachmentIds of attachments that need to be uploaded to the archive cdn.
*/
@@ -643,11 +637,7 @@ class AttachmentTable(
return readableDatabase
.select("$TABLE_NAME.$ID")
.from("$TABLE_NAME LEFT JOIN ${MessageTable.TABLE_NAME} ON $TABLE_NAME.$MESSAGE_ID = ${MessageTable.TABLE_NAME}.${MessageTable.ID}")
.where(
"($ARCHIVE_TRANSFER_STATE = ? or $ARCHIVE_TRANSFER_STATE = ?) AND $DATA_FILE NOT NULL AND $TRANSFER_STATE = $TRANSFER_PROGRESS_DONE AND (${MessageTable.STORY_TYPE} = 0 OR ${MessageTable.STORY_TYPE} IS NULL) AND ${getMessageDoesNotExpireWithinTimeoutClause()}",
ArchiveTransferState.NONE.value,
ArchiveTransferState.TEMPORARY_FAILURE.value
)
.where(buildAttachmentsThatNeedUploadQuery())
.orderBy("$TABLE_NAME.$ID DESC")
.run()
.readToList { AttachmentId(it.requireLong(ID)) }
@@ -691,14 +681,10 @@ class AttachmentTable(
/**
* Similar to [getAttachmentsThatNeedArchiveUpload], but returns if the list would be non-null in a more efficient way.
*/
fun doAnyAttachmentsNeedArchiveUpload(): Boolean {
fun doAnyAttachmentsNeedArchiveUpload(currentTime: Long): Boolean {
return readableDatabase
.exists("$TABLE_NAME LEFT JOIN ${MessageTable.TABLE_NAME} ON $TABLE_NAME.$MESSAGE_ID = ${MessageTable.TABLE_NAME}.${MessageTable.ID}")
.where(
"($ARCHIVE_TRANSFER_STATE = ? OR $ARCHIVE_TRANSFER_STATE = ?) AND $DATA_FILE NOT NULL AND $TRANSFER_STATE = $TRANSFER_PROGRESS_DONE AND (${MessageTable.STORY_TYPE} = 0 OR ${MessageTable.STORY_TYPE} IS NULL) AND ${getMessageDoesNotExpireWithinTimeoutClause()}",
ArchiveTransferState.NONE.value,
ArchiveTransferState.TEMPORARY_FAILURE.value
)
.where(buildAttachmentsThatNeedUploadQuery())
.run()
}
@@ -856,7 +842,7 @@ class AttachmentTable(
$REMOTE_KEY NOT NULL AND
$ARCHIVE_TRANSFER_STATE NOT IN (${ArchiveTransferState.FINISHED.value}, ${ArchiveTransferState.PERMANENT_FAILURE.value}) AND
(${MessageTable.STORY_TYPE} = 0 OR ${MessageTable.STORY_TYPE} IS NULL) AND
${getMessageDoesNotExpireWithinTimeoutClause()}
(${MessageTable.EXPIRES_IN} = 0 OR ${MessageTable.EXPIRES_IN} > ${ChatItemArchiveExporter.EXPIRATION_CUTOFF.inWholeMilliseconds})
)
""".trimIndent()
)
@@ -2547,6 +2533,16 @@ class AttachmentTable(
}
}
private fun buildAttachmentsThatNeedUploadQuery(): String {
return """
$ARCHIVE_TRANSFER_STATE IN (${ArchiveTransferState.NONE.value}, ${ArchiveTransferState.TEMPORARY_FAILURE.value}) AND
$DATA_FILE NOT NULL AND
$TRANSFER_STATE = $TRANSFER_PROGRESS_DONE AND
(${MessageTable.STORY_TYPE} = 0 OR ${MessageTable.STORY_TYPE} IS NULL) AND
(${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} <= 0 OR ${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} > ${ChatItemArchiveExporter.EXPIRATION_CUTOFF.inWholeMilliseconds}) AND
$CONTENT_TYPE != '${MediaUtil.LONG_TEXT}'
"""
}
private fun getAttachment(cursor: Cursor): DatabaseAttachment {
val contentType = cursor.requireString(CONTENT_TYPE)

View File

@@ -198,10 +198,12 @@ class AttachmentUploadJob private constructor(
messageId == AttachmentTable.PREUPLOAD_MESSAGE_ID -> {
Log.i(TAG, "[$attachmentId] Avoid uploading preuploaded attachments to archive. Skipping.")
}
SignalDatabase.messages.willMessageExpireBeforeCutoff(messageId) -> {
Log.i(TAG, "[$attachmentId] Message will expire within 24hrs. Skipping.")
}
databaseAttachment.contentType == MediaUtil.LONG_TEXT -> {
Log.i(TAG, "[$attachmentId] Long text attachment. Skipping.")
}
else -> {
Log.i(TAG, "[$attachmentId] Enqueuing job to copy to archive.")
AppDependencies.jobManager.add(CopyAttachmentToArchiveJob(attachmentId))

View File

@@ -240,7 +240,7 @@ class BackupMessagesJob private constructor(
return Result.failure()
}
if (SignalStore.backup.backsUpMedia && SignalDatabase.attachments.doAnyAttachmentsNeedArchiveUpload()) {
if (SignalStore.backup.backsUpMedia && SignalDatabase.attachments.doAnyAttachmentsNeedArchiveUpload(System.currentTimeMillis())) {
Log.i(TAG, "Enqueuing attachment backfill job.")
AppDependencies.jobManager.add(ArchiveAttachmentBackfillJob())
} else {

View File

@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobmanager.impl.NoRemoteArchiveGarbageCollectionPendingConstraint
import org.thoughtcrime.securesms.jobs.protos.CopyAttachmentToArchiveJobData
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.MediaUtil
import org.whispersystems.signalservice.api.NetworkResult
import java.util.concurrent.TimeUnit
@@ -102,6 +103,12 @@ class CopyAttachmentToArchiveJob private constructor(private val attachmentId: A
return Result.success()
}
if (attachment.contentType == MediaUtil.LONG_TEXT) {
Log.i(TAG, "[$attachmentId] Attachment is long text. Resetting transfer state to none and skipping.")
SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.NONE)
return Result.success()
}
if (isCanceled) {
Log.w(TAG, "[$attachmentId] Canceled. Refusing to proceed.")
return Result.failure()

View File

@@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.jobs.protos.UploadAttachmentToArchiveJobData
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.service.AttachmentProgressService
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.archive.ArchiveMediaUploadFormStatusCodes
@@ -137,6 +138,12 @@ class UploadAttachmentToArchiveJob private constructor(
return Result.success()
}
if (attachment.contentType == MediaUtil.LONG_TEXT) {
Log.i(TAG, "[$attachmentId] Attachment is long text. Resetting transfer state to none and skipping.")
SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.NONE)
return Result.success()
}
if (attachment.remoteKey == null) {
Log.w(TAG, "[$attachmentId] Attachment is missing remote key! Cannot upload.")
return Result.failure()