Allow backfill for more undownloadable attachments

This commit is contained in:
Fedor Indutny
2025-03-26 11:48:28 -07:00
committed by GitHub
parent d69887a4f9
commit 427f91f903
8 changed files with 98 additions and 49 deletions

View File

@@ -12,10 +12,7 @@ import {
AttachmentDownloadUrgency, AttachmentDownloadUrgency,
coreAttachmentDownloadJobSchema, coreAttachmentDownloadJobSchema,
} from '../types/AttachmentDownload'; } from '../types/AttachmentDownload';
import { import { downloadAttachment as downloadAttachmentUtil } from '../util/downloadAttachment';
AttachmentPermanentlyUndownloadableError,
downloadAttachment as downloadAttachmentUtil,
} from '../util/downloadAttachment';
import { DataWriter } from '../sql/Client'; import { DataWriter } from '../sql/Client';
import { getValue } from '../RemoteConfig'; import { getValue } from '../RemoteConfig';
@@ -24,6 +21,7 @@ import {
AttachmentSizeError, AttachmentSizeError,
type AttachmentType, type AttachmentType,
AttachmentVariant, AttachmentVariant,
AttachmentPermanentlyUndownloadableError,
mightBeOnBackupTier, mightBeOnBackupTier,
} from '../types/Attachment'; } from '../types/Attachment';
import { type ReadonlyMessageAttributesType } from '../model-types.d'; import { type ReadonlyMessageAttributesType } from '../model-types.d';
@@ -61,6 +59,7 @@ import { markAttachmentAsPermanentlyErrored } from '../util/attachments/markAtta
import { import {
AttachmentBackfill, AttachmentBackfill,
isPermanentlyUndownloadable, isPermanentlyUndownloadable,
isPermanentlyUndownloadableWithoutBackfill,
} from './helpers/attachmentBackfill'; } from './helpers/attachmentBackfill';
export { isPermanentlyUndownloadable }; export { isPermanentlyUndownloadable };
@@ -365,6 +364,16 @@ async function runDownloadAttachmentJob({
try { try {
log.info(`${logId}: Starting job`); log.info(`${logId}: Starting job`);
if (
job.source !== AttachmentDownloadSource.BACKFILL &&
isPermanentlyUndownloadableWithoutBackfill(job.attachment)
) {
// We should only get to here only if
throw new AttachmentPermanentlyUndownloadableError(
'Not downloadable without backfill'
);
}
const result = await runDownloadAttachmentJobInner({ const result = await runDownloadAttachmentJobInner({
job, job,
abortSignal: options.abortSignal, abortSignal: options.abortSignal,

View File

@@ -113,17 +113,6 @@ export class AttachmentBackfill {
AttachmentData: { Status }, AttachmentData: { Status },
} = Proto.SyncMessage.AttachmentBackfillResponse; } = Proto.SyncMessage.AttachmentBackfillResponse;
if ('error' in response) {
if (response.error === ErrorEnum.MESSAGE_NOT_FOUND) {
window.reduxActions.globalModals.showBackfillFailureModal({
kind: BackfillFailureKind.NotFound,
});
} else {
throw missingCaseError(response.error);
}
return;
}
const { targetMessage, targetConversation } = response; const { targetMessage, targetConversation } = response;
const convo = getConversationFromTarget(targetConversation); const convo = getConversationFromTarget(targetConversation);
@@ -141,13 +130,29 @@ export class AttachmentBackfill {
const message = window.MessageCache.register(new MessageModel(attributes)); const message = window.MessageCache.register(new MessageModel(attributes));
// IMPORTANT: no awaits until we finish modifying attachments
const timer = this.#pendingRequests.get(message.id); const timer = this.#pendingRequests.get(message.id);
if (timer != null) { if (timer != null) {
clearTimeout(timer); clearTimeout(timer);
} }
if ('error' in response) {
// Don't show error if we didn't request the data or already timed out
if (timer == null) {
return;
}
if (response.error === ErrorEnum.MESSAGE_NOT_FOUND) {
window.reduxActions.globalModals.showBackfillFailureModal({
kind: BackfillFailureKind.NotFound,
});
} else {
throw missingCaseError(response.error);
}
return;
}
// IMPORTANT: no awaits until we finish modifying attachments
// Since we are matching remote attachments with local attachments we need // Since we are matching remote attachments with local attachments we need
// to make sure things are normalized before starting. // to make sure things are normalized before starting.
message.set( message.set(
@@ -463,3 +468,24 @@ export function isPermanentlyUndownloadable(
// at this time. // at this time.
return !AttachmentBackfill.isEnabledForJob(disposition, message); return !AttachmentBackfill.isEnabledForJob(disposition, message);
} }
export function isPermanentlyUndownloadableWithoutBackfill(
attachment: AttachmentType
): boolean {
// Attachment is downloadable or user have not failed to download it yet
if (isDownloadable(attachment) || !attachment.error) {
return false;
}
// Too big attachments cannot be retried anymore
if (attachment.wasTooBig) {
return true;
}
// Previous backfill failed
if (attachment.backfillError) {
return true;
}
return true;
}

View File

@@ -7,16 +7,16 @@ import { noop } from 'lodash';
import { DataWriter } from '../../sql/Client'; import { DataWriter } from '../../sql/Client';
import { IMAGE_PNG } from '../../types/MIME'; import { IMAGE_PNG } from '../../types/MIME';
import { import { downloadAttachment } from '../../util/downloadAttachment';
AttachmentPermanentlyUndownloadableError,
downloadAttachment,
} from '../../util/downloadAttachment';
import { MediaTier } from '../../types/AttachmentDownload'; import { MediaTier } from '../../types/AttachmentDownload';
import { HTTPError } from '../../textsecure/Errors'; import { HTTPError } from '../../textsecure/Errors';
import { getCdnNumberForBackupTier } from '../../textsecure/downloadAttachment'; import { getCdnNumberForBackupTier } from '../../textsecure/downloadAttachment';
import { MASTER_KEY, MEDIA_ROOT_KEY } from '../backup/helpers'; import { MASTER_KEY, MEDIA_ROOT_KEY } from '../backup/helpers';
import { getMediaIdFromMediaName } from '../../services/backups/util/mediaId'; import { getMediaIdFromMediaName } from '../../services/backups/util/mediaId';
import { AttachmentVariant } from '../../types/Attachment'; import {
AttachmentVariant,
AttachmentPermanentlyUndownloadableError,
} from '../../types/Attachment';
describe('utils/downloadAttachment', () => { describe('utils/downloadAttachment', () => {
const baseAttachment = { const baseAttachment = {

View File

@@ -3825,14 +3825,6 @@ export default class MessageReceiver
logId logId
); );
let eventData: AttachmentBackfillResponseSyncEventData;
if (response.error != null) {
eventData = {
error: response.error,
targetMessage,
targetConversation,
};
} else {
strictAssert( strictAssert(
targetMessage != null, targetMessage != null,
'MessageReceiver.handleAttachmentBackfillResponse: invalid target message' 'MessageReceiver.handleAttachmentBackfillResponse: invalid target message'
@@ -3843,6 +3835,14 @@ export default class MessageReceiver
'invalid target conversation' 'invalid target conversation'
); );
let eventData: AttachmentBackfillResponseSyncEventData;
if (response.error != null) {
eventData = {
error: response.error,
targetMessage,
targetConversation,
};
} else {
const { attachments } = response; const { attachments } = response;
strictAssert( strictAssert(
attachments != null, attachments != null,

View File

@@ -17,6 +17,7 @@ import {
mightBeOnBackupTier, mightBeOnBackupTier,
type AttachmentType, type AttachmentType,
AttachmentVariant, AttachmentVariant,
AttachmentPermanentlyUndownloadableError,
} from '../types/Attachment'; } from '../types/Attachment';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import { import {
@@ -115,9 +116,13 @@ export async function downloadAttachment(
const { digest, incrementalMac, chunkSize, key, size } = attachment; const { digest, incrementalMac, chunkSize, key, size } = attachment;
try {
strictAssert(digest, `${logId}: missing digest`); strictAssert(digest, `${logId}: missing digest`);
strictAssert(key, `${logId}: missing key`); strictAssert(key, `${logId}: missing key`);
strictAssert(isNumber(size), `${logId}: missing size`); strictAssert(isNumber(size), `${logId}: missing size`);
} catch (error) {
throw new AttachmentPermanentlyUndownloadableError(error.message);
}
const mediaTier = const mediaTier =
options?.mediaTier ?? options?.mediaTier ??

View File

@@ -595,17 +595,18 @@ export type AttachmentBackfillAttachmentType = Readonly<
>; >;
export type AttachmentBackfillResponseSyncEventData = Readonly< export type AttachmentBackfillResponseSyncEventData = Readonly<
{
targetMessage: AddressableMessage;
targetConversation: ConversationIdentifier;
} & (
| { | {
error: Proto.SyncMessage.AttachmentBackfillResponse.Error; error: Proto.SyncMessage.AttachmentBackfillResponse.Error;
targetMessage?: AddressableMessage;
targetConversation?: ConversationIdentifier;
} }
| { | {
attachments: ReadonlyArray<AttachmentBackfillAttachmentType>; attachments: ReadonlyArray<AttachmentBackfillAttachmentType>;
longText: AttachmentBackfillAttachmentType | undefined; longText: AttachmentBackfillAttachmentType | undefined;
targetMessage: AddressableMessage;
targetConversation: ConversationIdentifier;
} }
)
>; >;
export class AttachmentBackfillResponseSyncEvent extends ConfirmableEvent { export class AttachmentBackfillResponseSyncEvent extends ConfirmableEvent {

View File

@@ -1,5 +1,6 @@
// Copyright 2018 Signal Messenger, LLC // Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
import moment from 'moment'; import moment from 'moment';
import { import {
@@ -46,6 +47,14 @@ const MIN_HEIGHT = 50;
export class AttachmentSizeError extends Error {} export class AttachmentSizeError extends Error {}
// Used for downlaods
export class AttachmentPermanentlyUndownloadableError extends Error {
constructor(message: string) {
super(`AttachmentPermanentlyUndownloadableError: ${message}`);
}
}
type ScreenshotType = Omit<AttachmentType, 'size'> & { type ScreenshotType = Omit<AttachmentType, 'size'> & {
height: number; height: number;
width: number; width: number;

View File

@@ -5,6 +5,7 @@ import {
type AttachmentType, type AttachmentType,
mightBeOnBackupTier, mightBeOnBackupTier,
AttachmentVariant, AttachmentVariant,
AttachmentPermanentlyUndownloadableError,
getAttachmentIdForLogging, getAttachmentIdForLogging,
} from '../types/Attachment'; } from '../types/Attachment';
import { downloadAttachment as doDownloadAttachment } from '../textsecure/downloadAttachment'; import { downloadAttachment as doDownloadAttachment } from '../textsecure/downloadAttachment';
@@ -14,8 +15,6 @@ import { HTTPError } from '../textsecure/Errors';
import { toLogFormat } from '../types/errors'; import { toLogFormat } from '../types/errors';
import type { ReencryptedAttachmentV2 } from '../AttachmentCrypto'; import type { ReencryptedAttachmentV2 } from '../AttachmentCrypto';
export class AttachmentPermanentlyUndownloadableError extends Error {}
export async function downloadAttachment({ export async function downloadAttachment({
attachment, attachment,
options: { variant = AttachmentVariant.Default, onSizeUpdate, abortSignal }, options: { variant = AttachmentVariant.Default, onSizeUpdate, abortSignal },
@@ -105,7 +104,7 @@ export async function downloadAttachment({
error instanceof HTTPError && error instanceof HTTPError &&
(error.code === 404 || error.code === 403) (error.code === 404 || error.code === 403)
) { ) {
throw new AttachmentPermanentlyUndownloadableError(); throw new AttachmentPermanentlyUndownloadableError(`HTTP ${error.code}`);
} else { } else {
throw error; throw error;
} }