Fix various issues with optimized media.

This commit is contained in:
Cody Henthorne
2025-11-19 12:46:02 -05:00
parent 6680e74cea
commit d918e11cab
13 changed files with 123 additions and 29 deletions

View File

@@ -589,6 +589,10 @@ object BackupRepository {
@JvmStatic
fun maybeFixAnyDanglingUploadProgress() {
if (SignalStore.account.isLinkedDevice) {
return
}
if (SignalStore.backup.archiveUploadState?.backupPhase == ArchiveUploadProgressState.BackupPhase.Message && AppDependencies.jobManager.find { it.factoryKey == BackupMessagesJob.KEY }.isEmpty()) {
SignalStore.backup.archiveUploadState = null
BackupMessagesJob.enqueue()

View File

@@ -403,7 +403,7 @@ public class ThumbnailView extends FrameLayout {
}
if (hasSameContents(this.slide, slide)) {
Log.i(TAG, "Not re-loading slide " + slide.asAttachment().getUri());
Log.i(TAG, "Not re-loading slide " + slide.asAttachment().getDisplayUri());
return new SettableFuture<>(false);
}

View File

@@ -869,6 +869,7 @@ class AttachmentTable(
"""
${buildAttachmentsThatNeedUploadQuery("$ARCHIVE_THUMBNAIL_TRANSFER_STATE IN (${ArchiveTransferState.NONE.value}, ${ArchiveTransferState.TEMPORARY_FAILURE.value})")} AND
$QUOTE = 0 AND
$STICKER_ID = -1 AND
($CONTENT_TYPE LIKE 'image/%' OR $CONTENT_TYPE LIKE 'video/%') AND
$CONTENT_TYPE != 'image/svg+xml' AND
$MESSAGE_ID != $WALLPAPER_MESSAGE_ID
@@ -888,6 +889,7 @@ class AttachmentTable(
"""
${buildAttachmentsThatNeedUploadQuery("$ARCHIVE_THUMBNAIL_TRANSFER_STATE IN (${ArchiveTransferState.NONE.value}, ${ArchiveTransferState.TEMPORARY_FAILURE.value})")} AND
$QUOTE = 0 AND
$STICKER_ID = -1 AND
($CONTENT_TYPE LIKE 'image/%' OR $CONTENT_TYPE LIKE 'video/%') AND
$CONTENT_TYPE != 'image/svg+xml' AND
$MESSAGE_ID != $WALLPAPER_MESSAGE_ID
@@ -1122,11 +1124,15 @@ class AttachmentTable(
$TABLE_NAME.$OFFLOAD_RESTORED_AT < ${now - 7.days.inWholeMilliseconds} AND
$TABLE_NAME.$TRANSFER_STATE = $TRANSFER_PROGRESS_DONE AND
$TABLE_NAME.$ARCHIVE_TRANSFER_STATE = ${ArchiveTransferState.FINISHED.value} AND
$TABLE_NAME.$DATA_FILE IS NOT NULL AND
$TABLE_NAME.$STICKER_ID = -1 AND
$TABLE_NAME.$REMOTE_KEY IS NOT NULL AND
$TABLE_NAME.$DATA_HASH_END IS NOT NULL AND
(
$TABLE_NAME.$THUMBNAIL_FILE IS NOT NULL OR
NOT ($TABLE_NAME.$CONTENT_TYPE like 'image/%' OR $TABLE_NAME.$CONTENT_TYPE like 'video/%')
) AND
$TABLE_NAME.$DATA_FILE IS NOT NULL
NOT ($TABLE_NAME.$CONTENT_TYPE LIKE 'image/%' OR $TABLE_NAME.$CONTENT_TYPE LIKE 'video/%') OR
$TABLE_NAME.$CONTENT_TYPE = 'image/svg+xml'
)
)
AND
(
@@ -1134,7 +1140,7 @@ class AttachmentTable(
)
"""
writableDatabase
val count = writableDatabase
.update(TABLE_NAME)
.values(
TRANSFER_STATE to TRANSFER_RESTORE_OFFLOADED,
@@ -1142,11 +1148,12 @@ class AttachmentTable(
DATA_RANDOM to null,
TRANSFORM_PROPERTIES to null,
DATA_HASH_START to null,
DATA_HASH_END to null,
OFFLOAD_RESTORED_AT to 0
)
.where("$ID in ($subSelect)")
.run()
Log.i(TAG, "Marked $count attachments as optimized")
}
/**

View File

@@ -54,7 +54,8 @@ class OptimizeMediaJob private constructor(parameters: Parameters) : Job(paramet
SignalDatabase.attachments.markEligibleAttachmentsAsOptimized()
Log.i(TAG, "Deleting abandoned attachment files")
SignalDatabase.attachments.deleteAbandonedAttachmentFiles()
val count = SignalDatabase.attachments.deleteAbandonedAttachmentFiles()
Log.i(TAG, "Deleted $count attachments")
return Result.success()
}

View File

@@ -164,7 +164,7 @@ class RestoreAttachmentJob private constructor(
messageId = attachment.mmsId,
attachmentId = attachment.attachmentId,
manual = true,
queue = Queues.MANUAL_RESTORE.random(),
queue = Queues.random(Queues.MANUAL_RESTORE, attachment.dataHash?.hashCode() ?: attachment.remoteKey?.hashCode()),
priority = Parameters.PRIORITY_DEFAULT
)
@@ -410,7 +410,7 @@ class RestoreAttachmentJob private constructor(
inputStream = input,
offloadRestoredAt = if (manual) System.currentTimeMillis().milliseconds else null,
archiveRestore = true,
notify = false
notify = manual
)
ArchiveDatabaseExecutor.throttledNotifyAttachmentAndChatListObservers()
}

View File

@@ -251,12 +251,13 @@ public abstract class Slide {
this.hasImage() == that.hasImage() &&
this.hasVideo() == that.hasVideo() &&
this.getTransferState() == that.getTransferState() &&
Util.equals(this.getUri(), that.getUri());
Util.equals(this.getUri(), that.getUri()) &&
Util.equals(this.getThumbnailUri(), that.getThumbnailUri());
}
@Override
public int hashCode() {
return Util.hashCode(getContentType(), hasAudio(), hasImage(),
hasVideo(), getUri(), getTransferState());
hasVideo(), getUri(), getTransferState(), getThumbnailUri());
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms
import okio.IOException
import org.signal.spinner.Plugin
import org.signal.spinner.PluginResult
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.SignalDatabase
class AttachmentPlugin : Plugin {
companion object {
const val PATH = "/attachment"
}
override val name: String = "Attachment"
override val path: String = PATH
override fun get(parameters: Map<String, List<String>>): PluginResult {
var errorContent = ""
parameters["attachment_id"]?.firstOrNull()?.let { id ->
val attachmentId = id.toLongOrNull()?.let { AttachmentId(it) }
if (attachmentId != null) {
try {
val attachment = SignalDatabase.attachments.getAttachment(attachmentId)
if (attachment != null) {
val inputStream = if (attachment.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE) {
SignalDatabase.attachments.getAttachmentStream(attachmentId, 0)
} else {
SignalDatabase.attachments.getAttachmentThumbnailStream(attachmentId, 0)
}
return PluginResult.RawFileResult(attachment.size, inputStream, attachment.contentType ?: "application/octet-stream")
} else {
throw IOException("Missing attachment, not found for: $attachmentId")
}
} catch (e: IOException) {
errorContent = "${e.javaClass}: ${e.message}"
}
}
}
val formContent = """
<form action="$PATH" method="GET">
<label for="number">Enter an attachment_id:</label>
<input type="number" id="attachment_id" name="attachment_id" required>
<button type="submit">Submit</button>
</form>
""".trimIndent()
return PluginResult.RawHtmlResult("$formContent<br>$errorContent")
}
}

View File

@@ -85,7 +85,8 @@ class SpinnerApplicationContext : ApplicationContext() {
)
),
linkedMapOf(
StorageServicePlugin.PATH to StorageServicePlugin()
StorageServicePlugin.PATH to StorageServicePlugin(),
AttachmentPlugin.PATH to AttachmentPlugin()
)
)

View File

@@ -11,7 +11,7 @@ class StorageServicePlugin : Plugin {
override val name: String = "Storage"
override val path: String = PATH
override fun get(): PluginResult {
override fun get(parameters: Map<String, List<String>>): PluginResult {
val columns = listOf("Type", "Id", "Data")
val rows = mutableListOf<List<String>>()

View File

@@ -26,6 +26,9 @@
{{#if (eq "string" pluginResult.type)}}
<p>{{pluginResult.text}}</p>
{{/if}}
{{#if (eq "html" pluginResult.type)}}
{{{pluginResult.html}}}
{{/if}}
{{> partials/suffix }}
</body>
</html>

View File

@@ -1,7 +1,7 @@
package org.signal.spinner
interface Plugin {
fun get(): PluginResult
fun get(parameters: Map<String, List<String>>): PluginResult
val name: String
val path: String
}

View File

@@ -1,5 +1,7 @@
package org.signal.spinner
import java.io.InputStream
sealed class PluginResult(val type: String) {
data class TableResult(
val columns: List<String>,
@@ -10,4 +12,14 @@ sealed class PluginResult(val type: String) {
data class StringResult(
val text: String
) : PluginResult("string")
data class RawHtmlResult(
val html: String
) : PluginResult("html")
data class RawFileResult(
val length: Long,
val data: InputStream,
val mimeType: String
) : PluginResult("file")
}

View File

@@ -15,7 +15,6 @@ import org.signal.core.util.logging.Log
import org.signal.core.util.tracing.Tracer
import org.signal.spinner.Spinner.DatabaseConfig
import java.io.ByteArrayInputStream
import java.lang.IllegalArgumentException
import java.security.NoSuchAlgorithmException
import java.text.SimpleDateFormat
import java.util.Date
@@ -79,7 +78,7 @@ internal class SpinnerServer(
else -> {
val plugin = plugins[session.uri]
if (plugin != null && session.method == Method.GET) {
getPlugin(dbParam, plugin)
getPlugin(dbParam, plugin, session.parameters)
} else {
newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "Not found")
}
@@ -280,19 +279,28 @@ internal class SpinnerServer(
)
}
private fun getPlugin(dbName: String, plugin: Plugin): Response {
return renderTemplate(
"plugin",
PluginPageModel(
environment = environment,
deviceInfo = deviceInfo.resolve(),
database = dbName,
databases = databases.keys.toList(),
plugins = plugins.values.toList(),
activePlugin = plugin,
pluginResult = plugin.get()
)
)
private fun getPlugin(dbName: String, plugin: Plugin, parameters: Map<String, List<String>>): Response {
val pluginResult = plugin.get(parameters)
when (pluginResult) {
is PluginResult.RawFileResult -> {
return newFixedLengthResponse(Response.Status.OK, pluginResult.mimeType, pluginResult.data, pluginResult.length)
}
else -> {
return renderTemplate(
"plugin",
PluginPageModel(
environment = environment,
deviceInfo = deviceInfo.resolve(),
database = dbName,
databases = databases.keys.toList(),
plugins = plugins.values.toList(),
activePlugin = plugin,
pluginResult = plugin.get(parameters)
)
)
}
}
}
private fun internalError(throwable: Throwable): Response {