Fix various archive related attachment bugs.

Co-authored-by: Greyson Parrelli <greyson@signal.org>
This commit is contained in:
Cody Henthorne
2025-05-29 11:34:09 -04:00
committed by GitHub
parent 4b41d7afaa
commit 7b30ac1524
10 changed files with 86 additions and 30 deletions

View File

@@ -182,7 +182,7 @@ class BackupMediaSnapshotTableTest {
val notFound = SignalDatabase.backupMediaSnapshots.getMediaObjectsThatCantBeFound(remoteData) val notFound = SignalDatabase.backupMediaSnapshots.getMediaObjectsThatCantBeFound(remoteData)
assertThat(notFound.size).isEqualTo(1) assertThat(notFound.size).isEqualTo(1)
assertThat(notFound.first().cdn).isEqualTo(2) assertThat(notFound.first()).isEqualTo(remoteData[1])
} }
private fun getTotalItemCount(): Int { private fun getTotalItemCount(): Int {

View File

@@ -290,6 +290,10 @@ class AttachmentTable(
"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);"
) )
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
)
@JvmStatic @JvmStatic
@Throws(IOException::class) @Throws(IOException::class)
fun newDataFile(context: Context): File { fun newDataFile(context: Context): File {
@@ -571,7 +575,7 @@ class AttachmentTable(
return readableDatabase return readableDatabase
.select(ID) .select(ID)
.from(TABLE_NAME) .from(TABLE_NAME)
.where("$ARCHIVE_TRANSFER_STATE = ? AND $DATA_FILE NOT NULL AND $TRANSFER_STATE = $TRANSFER_PROGRESS_DONE", ArchiveTransferState.NONE.value) .where("($ARCHIVE_TRANSFER_STATE = ? or $ARCHIVE_TRANSFER_STATE = ?) AND $DATA_FILE NOT NULL AND $TRANSFER_STATE = $TRANSFER_PROGRESS_DONE", ArchiveTransferState.NONE.value, ArchiveTransferState.TEMPORARY_FAILURE.value)
.orderBy("$ID DESC") .orderBy("$ID DESC")
.run() .run()
.readToList { AttachmentId(it.requireLong(ID)) } .readToList { AttachmentId(it.requireLong(ID)) }
@@ -636,7 +640,7 @@ class AttachmentTable(
fun doAnyAttachmentsNeedArchiveUpload(): Boolean { fun doAnyAttachmentsNeedArchiveUpload(): Boolean {
return readableDatabase return readableDatabase
.exists(TABLE_NAME) .exists(TABLE_NAME)
.where("$ARCHIVE_TRANSFER_STATE = ? AND $DATA_FILE NOT NULL AND $TRANSFER_STATE = $TRANSFER_PROGRESS_DONE", ArchiveTransferState.NONE.value) .where("($ARCHIVE_TRANSFER_STATE = ? OR $ARCHIVE_TRANSFER_STATE = ?) AND $DATA_FILE NOT NULL AND $TRANSFER_STATE = $TRANSFER_PROGRESS_DONE", ArchiveTransferState.NONE.value, ArchiveTransferState.TEMPORARY_FAILURE.value)
.run() .run()
} }
@@ -672,6 +676,27 @@ class AttachmentTable(
} }
} }
/**
* Sets the archive transfer state for the given attachment and all other attachments that share the same data file iff
* the row isn't already marked as a [ArchiveTransferState.PERMANENT_FAILURE].
*/
fun setArchiveTransferStateFailure(id: AttachmentId, state: ArchiveTransferState) {
writableDatabase.withinTransaction {
val dataFile: String = readableDatabase
.select(DATA_FILE)
.from(TABLE_NAME)
.where("$ID = ?", id.id)
.run()
.readToSingleObject { it.requireString(DATA_FILE) } ?: return@withinTransaction
writableDatabase
.update(TABLE_NAME)
.values(ARCHIVE_TRANSFER_STATE to state.value)
.where("$ARCHIVE_TRANSFER_STATE != ? AND $DATA_FILE = ?", ArchiveTransferState.PERMANENT_FAILURE.value, dataFile)
.run()
}
}
/** /**
* Sets the archive transfer state for the given attachment by digest. * Sets the archive transfer state for the given attachment by digest.
*/ */
@@ -1180,7 +1205,7 @@ class AttachmentTable(
// We don't look at hash_start here because that could result in us matching on a file that got compressed down to something smaller, effectively lowering // We don't look at hash_start here because that could result in us matching on a file that got compressed down to something smaller, effectively lowering
// the quality of the attachment we received. // the quality of the attachment we received.
val hashMatch: DataFileInfo? = readableDatabase val hashMatch: DataFileInfo? = readableDatabase
.select(ID, DATA_FILE, DATA_SIZE, DATA_RANDOM, DATA_HASH_START, DATA_HASH_END, TRANSFORM_PROPERTIES, UPLOAD_TIMESTAMP, ARCHIVE_CDN) .select(*DATA_FILE_INFO_PROJECTION)
.from(TABLE_NAME) .from(TABLE_NAME)
.where("$DATA_HASH_END = ? AND $DATA_HASH_END NOT NULL AND $TRANSFER_STATE = $TRANSFER_PROGRESS_DONE AND $DATA_FILE NOT NULL", fileWriteResult.hash) .where("$DATA_HASH_END = ? AND $DATA_HASH_END NOT NULL AND $TRANSFER_STATE = $TRANSFER_PROGRESS_DONE AND $DATA_FILE NOT NULL", fileWriteResult.hash)
.run() .run()
@@ -1197,6 +1222,10 @@ class AttachmentTable(
values.put(DATA_HASH_START, hashMatch.hashEnd) values.put(DATA_HASH_START, hashMatch.hashEnd)
values.put(DATA_HASH_END, hashMatch.hashEnd) values.put(DATA_HASH_END, hashMatch.hashEnd)
values.put(ARCHIVE_CDN, hashMatch.archiveCdn) values.put(ARCHIVE_CDN, hashMatch.archiveCdn)
values.put(ARCHIVE_TRANSFER_STATE, hashMatch.archiveTransferState)
values.put(THUMBNAIL_FILE, hashMatch.thumbnailFile)
values.put(THUMBNAIL_RANDOM, hashMatch.thumbnailRandom)
values.put(THUMBNAIL_RESTORE_STATE, hashMatch.thumbnailRestoreState)
} else { } else {
values.put(DATA_FILE, fileWriteResult.file.absolutePath) values.put(DATA_FILE, fileWriteResult.file.absolutePath)
values.put(DATA_SIZE, fileWriteResult.length) values.put(DATA_SIZE, fileWriteResult.length)
@@ -1748,7 +1777,7 @@ class AttachmentTable(
fun getDataFileInfo(attachmentId: AttachmentId): DataFileInfo? { fun getDataFileInfo(attachmentId: AttachmentId): DataFileInfo? {
return readableDatabase return readableDatabase
.select(ID, DATA_FILE, DATA_SIZE, DATA_RANDOM, DATA_HASH_START, DATA_HASH_END, TRANSFORM_PROPERTIES, UPLOAD_TIMESTAMP, ARCHIVE_CDN) .select(*DATA_FILE_INFO_PROJECTION)
.from(TABLE_NAME) .from(TABLE_NAME)
.where("$ID = ?", attachmentId.id) .where("$ID = ?", attachmentId.id)
.run() .run()
@@ -2402,7 +2431,7 @@ class AttachmentTable(
// First we'll check if our file hash matches the starting or ending hash of any other attachments and has compatible transform properties. // First we'll check if our file hash matches the starting or ending hash of any other attachments and has compatible transform properties.
// We'll prefer the match with the most recent upload timestamp. // We'll prefer the match with the most recent upload timestamp.
val hashMatch: DataFileInfo? = readableDatabase val hashMatch: DataFileInfo? = readableDatabase
.select(ID, DATA_FILE, DATA_SIZE, DATA_RANDOM, DATA_HASH_START, DATA_HASH_END, TRANSFORM_PROPERTIES, UPLOAD_TIMESTAMP, ARCHIVE_CDN) .select(*DATA_FILE_INFO_PROJECTION)
.from(TABLE_NAME) .from(TABLE_NAME)
.where("$DATA_FILE NOT NULL AND ($DATA_HASH_START = ? OR $DATA_HASH_END = ?)", fileWriteResult.hash, fileWriteResult.hash) .where("$DATA_FILE NOT NULL AND ($DATA_HASH_START = ? OR $DATA_HASH_END = ?)", fileWriteResult.hash, fileWriteResult.hash)
.run() .run()
@@ -2438,6 +2467,10 @@ class AttachmentTable(
contentValues.put(DATA_HASH_START, fileWriteResult.hash) contentValues.put(DATA_HASH_START, fileWriteResult.hash)
contentValues.put(DATA_HASH_END, hashMatch.hashEnd) contentValues.put(DATA_HASH_END, hashMatch.hashEnd)
contentValues.put(ARCHIVE_CDN, hashMatch.archiveCdn) contentValues.put(ARCHIVE_CDN, hashMatch.archiveCdn)
contentValues.put(ARCHIVE_TRANSFER_STATE, hashMatch.archiveTransferState)
contentValues.put(THUMBNAIL_FILE, hashMatch.thumbnailFile)
contentValues.put(THUMBNAIL_RANDOM, hashMatch.thumbnailRandom)
contentValues.put(THUMBNAIL_RESTORE_STATE, hashMatch.thumbnailRestoreState)
if (hashMatch.transformProperties.skipTransform) { 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})") 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})")
@@ -2625,7 +2658,11 @@ class AttachmentTable(
hashEnd = this.requireString(DATA_HASH_END), hashEnd = this.requireString(DATA_HASH_END),
transformProperties = TransformProperties.parse(this.requireString(TRANSFORM_PROPERTIES)), transformProperties = TransformProperties.parse(this.requireString(TRANSFORM_PROPERTIES)),
uploadTimestamp = this.requireLong(UPLOAD_TIMESTAMP), uploadTimestamp = this.requireLong(UPLOAD_TIMESTAMP),
archiveCdn = this.requireInt(ARCHIVE_CDN) archiveCdn = this.requireInt(ARCHIVE_CDN),
archiveTransferState = this.requireInt(ARCHIVE_TRANSFER_STATE),
thumbnailFile = this.requireString(THUMBNAIL_FILE),
thumbnailRandom = this.requireBlob(THUMBNAIL_RANDOM),
thumbnailRestoreState = this.requireInt(THUMBNAIL_RESTORE_STATE)
) )
} }
@@ -2689,7 +2726,11 @@ class AttachmentTable(
val hashEnd: String?, val hashEnd: String?,
val transformProperties: TransformProperties, val transformProperties: TransformProperties,
val uploadTimestamp: Long, val uploadTimestamp: Long,
val archiveCdn: Int val archiveCdn: Int,
val archiveTransferState: Int,
val thumbnailFile: String?,
val thumbnailRandom: ByteArray?,
val thumbnailRestoreState: Int
) )
@VisibleForTesting @VisibleForTesting

View File

@@ -137,35 +137,31 @@ class BackupMediaSnapshotTable(context: Context, database: SignalDatabase) : Dat
/** /**
* Given a list of media objects, find the ones that we have no knowledge of in our local store. * Given a list of media objects, find the ones that we have no knowledge of in our local store.
*/ */
fun getMediaObjectsThatCantBeFound(objects: List<ArchivedMediaObject>): Set<ArchivedMediaObject> { fun getMediaObjectsThatCantBeFound(objects: List<ArchivedMediaObject>): List<ArchivedMediaObject> {
if (objects.isEmpty()) { if (objects.isEmpty()) {
return emptySet() return emptyList()
} }
val queries: List<SqlUtil.Query> = SqlUtil.buildCollectionQuery( val queries: List<SqlUtil.Query> = SqlUtil.buildCollectionQuery(
column = MEDIA_ID, column = MEDIA_ID,
values = objects.map { it.mediaId }, values = objects.map { it.mediaId },
collectionOperator = SqlUtil.CollectionOperator.NOT_IN, collectionOperator = SqlUtil.CollectionOperator.IN
prefix = "$IS_THUMBNAIL = 0 AND "
) )
val out: MutableSet<ArchivedMediaObject> = mutableSetOf() val foundObjects: MutableSet<String> = mutableSetOf()
for (query in queries) { for (query in queries) {
out += readableDatabase foundObjects += readableDatabase
.select(MEDIA_ID, CDN) .select(MEDIA_ID, CDN)
.from(TABLE_NAME) .from(TABLE_NAME)
.where(query.where, query.whereArgs) .where(query.where, query.whereArgs)
.run() .run()
.readToSet { .readToSet {
ArchivedMediaObject( it.requireNonNullString(MEDIA_ID)
mediaId = it.requireNonNullString(MEDIA_ID),
cdn = it.requireInt(CDN)
)
} }
} }
return out return objects.filterNot { foundObjects.contains(it.mediaId) }
} }
/** /**

View File

@@ -165,10 +165,10 @@ class BackupMediaSnapshotSyncJob private constructor(
} }
/** /**
* Update CDNs of archived media items. Returns set of objects that don't match * Update CDNs of archived media items. Returns list of objects that don't match
* to a local attachment DB row. * to a local attachment DB row.
*/ */
private fun syncCdnPage(archivedItemPage: ArchiveGetMediaItemsResponse): Set<ArchivedMediaObject> { private fun syncCdnPage(archivedItemPage: ArchiveGetMediaItemsResponse): List<ArchivedMediaObject> {
val mediaObjects = archivedItemPage.storedMediaObjects.map { val mediaObjects = archivedItemPage.storedMediaObjects.map {
ArchivedMediaObject( ArchivedMediaObject(
mediaId = it.mediaId, mediaId = it.mediaId,

View File

@@ -118,7 +118,7 @@ class CopyAttachmentToArchiveJob private constructor(private val attachmentId: A
410 -> { 410 -> {
Log.w(TAG, "[$attachmentId] The attachment no longer exists on the transit tier. Scheduling a re-upload.") Log.w(TAG, "[$attachmentId] The attachment no longer exists on the transit tier. Scheduling a re-upload.")
SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.NONE) SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.NONE)
AppDependencies.jobManager.add(UploadAttachmentToArchiveJob(attachmentId)) AppDependencies.jobManager.add(UploadAttachmentToArchiveJob(attachmentId, canReuseUpload = false))
Result.success() Result.success()
} }
413 -> { 413 -> {
@@ -163,7 +163,7 @@ class CopyAttachmentToArchiveJob private constructor(private val attachmentId: A
SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.COPY_PENDING) SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.COPY_PENDING)
} else { } else {
Log.w(TAG, "[$attachmentId] Job failed, updating archive transfer state to ${AttachmentTable.ArchiveTransferState.TEMPORARY_FAILURE}.") Log.w(TAG, "[$attachmentId] Job failed, updating archive transfer state to ${AttachmentTable.ArchiveTransferState.TEMPORARY_FAILURE}.")
SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.TEMPORARY_FAILURE) SignalDatabase.attachments.setArchiveTransferStateFailure(attachmentId, AttachmentTable.ArchiveTransferState.TEMPORARY_FAILURE)
} }
ArchiveUploadProgress.onAttachmentFinished(attachmentId) ArchiveUploadProgress.onAttachmentFinished(attachmentId)

View File

@@ -6,6 +6,8 @@
package org.thoughtcrime.securesms.jobs package org.thoughtcrime.securesms.jobs
import org.signal.core.util.Base64 import org.signal.core.util.Base64
import org.signal.core.util.inRoundedDays
import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.core.util.readLength import org.signal.core.util.readLength
import org.signal.protos.resumableuploads.ResumableUpload import org.signal.protos.resumableuploads.ResumableUpload
@@ -32,6 +34,7 @@ import java.net.ProtocolException
import kotlin.random.Random import kotlin.random.Random
import kotlin.random.nextInt import kotlin.random.nextInt
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
/** /**
* Given an attachmentId, this will upload the corresponding attachment to the archive cdn. * Given an attachmentId, this will upload the corresponding attachment to the archive cdn.
@@ -40,6 +43,7 @@ import kotlin.time.Duration.Companion.days
class UploadAttachmentToArchiveJob private constructor( class UploadAttachmentToArchiveJob private constructor(
private val attachmentId: AttachmentId, private val attachmentId: AttachmentId,
private var uploadSpec: ResumableUpload?, private var uploadSpec: ResumableUpload?,
private val canReuseUpload: Boolean,
parameters: Parameters parameters: Parameters
) : Job(parameters) { ) : Job(parameters) {
@@ -59,9 +63,10 @@ class UploadAttachmentToArchiveJob private constructor(
fun getAllQueueKeys() = (0 until MAX_JOB_QUEUES).map { buildQueueKey(queue = it) } fun getAllQueueKeys() = (0 until MAX_JOB_QUEUES).map { buildQueueKey(queue = it) }
} }
constructor(attachmentId: AttachmentId) : this( constructor(attachmentId: AttachmentId, canReuseUpload: Boolean = true) : this(
attachmentId = attachmentId, attachmentId = attachmentId,
uploadSpec = null, uploadSpec = null,
canReuseUpload = canReuseUpload,
parameters = Parameters.Builder() parameters = Parameters.Builder()
.addConstraint(NetworkConstraint.KEY) .addConstraint(NetworkConstraint.KEY)
.setLifespan(30.days.inWholeMilliseconds) .setLifespan(30.days.inWholeMilliseconds)
@@ -71,7 +76,9 @@ class UploadAttachmentToArchiveJob private constructor(
) )
override fun serialize(): ByteArray = UploadAttachmentToArchiveJobData( override fun serialize(): ByteArray = UploadAttachmentToArchiveJobData(
attachmentId = attachmentId.id attachmentId = attachmentId.id,
uploadSpec = uploadSpec,
canReuseUpload = canReuseUpload
).encode() ).encode()
override fun getFactoryKey(): String = KEY override fun getFactoryKey(): String = KEY
@@ -120,6 +127,13 @@ class UploadAttachmentToArchiveJob private constructor(
return Result.failure() return Result.failure()
} }
val timeSinceUpload = System.currentTimeMillis() - attachment.uploadTimestamp
if (canReuseUpload && timeSinceUpload > 0 && timeSinceUpload < AttachmentUploadJob.UPLOAD_REUSE_THRESHOLD && attachment.remoteLocation.isNotNullOrBlank()) {
Log.i(TAG, "We can copy an already-uploaded file. It was uploaded $timeSinceUpload ms (${timeSinceUpload.milliseconds.inRoundedDays()} days) ago. Skipping.")
AppDependencies.jobManager.add(CopyAttachmentToArchiveJob(attachment.attachmentId))
return Result.success()
}
if (uploadSpec != null && System.currentTimeMillis() > uploadSpec!!.timeout) { if (uploadSpec != null && System.currentTimeMillis() > uploadSpec!!.timeout) {
Log.w(TAG, "[$attachmentId] Upload spec expired! Clearing.") Log.w(TAG, "[$attachmentId] Upload spec expired! Clearing.")
uploadSpec = null uploadSpec = null
@@ -200,10 +214,10 @@ class UploadAttachmentToArchiveJob private constructor(
override fun onFailure() { override fun onFailure() {
if (this.isCanceled) { if (this.isCanceled) {
Log.w(TAG, "[$attachmentId] Job was canceled, updating archive transfer state to ${AttachmentTable.ArchiveTransferState.NONE}.") Log.w(TAG, "[$attachmentId] Job was canceled, updating archive transfer state to ${AttachmentTable.ArchiveTransferState.NONE}.")
SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.NONE) SignalDatabase.attachments.setArchiveTransferStateFailure(attachmentId, AttachmentTable.ArchiveTransferState.NONE)
} else { } else {
Log.w(TAG, "[$attachmentId] Job failed, updating archive transfer state to ${AttachmentTable.ArchiveTransferState.TEMPORARY_FAILURE} (if not already a permanent failure).") Log.w(TAG, "[$attachmentId] Job failed, updating archive transfer state to ${AttachmentTable.ArchiveTransferState.TEMPORARY_FAILURE} (if not already a permanent failure).")
SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.TEMPORARY_FAILURE) SignalDatabase.attachments.setArchiveTransferStateFailure(attachmentId, AttachmentTable.ArchiveTransferState.TEMPORARY_FAILURE)
} }
} }
@@ -253,6 +267,7 @@ class UploadAttachmentToArchiveJob private constructor(
return UploadAttachmentToArchiveJob( return UploadAttachmentToArchiveJob(
attachmentId = AttachmentId(data.attachmentId), attachmentId = AttachmentId(data.attachmentId),
uploadSpec = data.uploadSpec, uploadSpec = data.uploadSpec,
canReuseUpload = data.canReuseUpload == true,
parameters = parameters parameters = parameters
) )
} }

View File

@@ -35,7 +35,7 @@ public final class WallpaperStorage {
AttachmentId attachmentId = SignalDatabase.attachments().insertWallpaper(wallpaperStream); AttachmentId attachmentId = SignalDatabase.attachments().insertWallpaper(wallpaperStream);
if (SignalStore.backup().backsUpMedia()) { if (SignalStore.backup().backsUpMedia()) {
AppDependencies.getJobManager().add(new UploadAttachmentToArchiveJob(attachmentId)); AppDependencies.getJobManager().add(new UploadAttachmentToArchiveJob(attachmentId, true));
} }
return ChatWallpaperFactory.create(PartAuthority.getAttachmentDataUri(attachmentId)); return ChatWallpaperFactory.create(PartAuthority.getAttachmentDataUri(attachmentId));

View File

@@ -138,6 +138,7 @@ message CopyAttachmentToArchiveJobData {
message UploadAttachmentToArchiveJobData { message UploadAttachmentToArchiveJobData {
uint64 attachmentId = 1; uint64 attachmentId = 1;
ResumableUpload uploadSpec = 2; ResumableUpload uploadSpec = 2;
optional bool canReuseUpload = 3;
} }
message BackupMediaSnapshotSyncJobData { message BackupMediaSnapshotSyncJobData {

View File

@@ -105,10 +105,12 @@ sealed class NetworkResult<T>(
@JvmStatic @JvmStatic
fun fromWebSocketRequest( fun fromWebSocketRequest(
signalWebSocket: SignalWebSocket, signalWebSocket: SignalWebSocket,
request: WebSocketRequestMessage request: WebSocketRequestMessage,
timeout: Duration = WebSocketConnection.DEFAULT_SEND_TIMEOUT
): NetworkResult<Unit> = fromWebSocketRequest( ): NetworkResult<Unit> = fromWebSocketRequest(
signalWebSocket = signalWebSocket, signalWebSocket = signalWebSocket,
request = request, request = request,
timeout = timeout,
clazz = Unit::class clazz = Unit::class
) )

View File

@@ -31,6 +31,7 @@ import java.io.InputStream
import java.time.Instant import java.time.Instant
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/** /**
* Class to interact with various archive-related endpoints. * Class to interact with various archive-related endpoints.
@@ -346,7 +347,7 @@ class ArchiveApi(
.map { it.toArchiveCredentialPresentation().toHeaders() } .map { it.toArchiveCredentialPresentation().toHeaders() }
.then { headers -> .then { headers ->
val request = WebSocketRequestMessage.post("/v1/archives/media/delete", DeleteArchivedMediaRequest(mediaToDelete = mediaToDelete), headers) val request = WebSocketRequestMessage.post("/v1/archives/media/delete", DeleteArchivedMediaRequest(mediaToDelete = mediaToDelete), headers)
NetworkResult.fromWebSocketRequest(unauthWebSocket, request) NetworkResult.fromWebSocketRequest(unauthWebSocket, request, timeout = 30.seconds)
} }
} }