signal-desktop/ts/util/downloadAttachment.ts

187 lines
5.8 KiB
TypeScript
Raw Normal View History

2020-10-30 15:34:04 -05:00
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
2025-09-08 16:19:17 -04:00
import { ErrorCode, LibSignalErrorBase } from '@signalapp/libsignal-client';
import {
type AttachmentType,
AttachmentVariant,
AttachmentPermanentlyUndownloadableError,
hasRequiredInformationForBackup,
wasImportedFromLocalBackup,
} from '../types/Attachment.js';
import { downloadAttachment as doDownloadAttachment } from '../textsecure/downloadAttachment.js';
import { downloadAttachmentFromLocalBackup as doDownloadAttachmentFromLocalBackup } from './downloadAttachmentFromLocalBackup.js';
import { MediaTier } from '../types/AttachmentDownload.js';
import { createLogger } from '../logging/log.js';
import { HTTPError } from '../textsecure/Errors.js';
import { toLogFormat } from '../types/errors.js';
import type { ReencryptedAttachmentV2 } from '../AttachmentCrypto.js';
import * as RemoteConfig from '../RemoteConfig.js';
import { ToastType } from '../types/Toast.js';
import { isAbortError } from './isAbortError.js';
2025-06-16 11:59:31 -07:00
const log = createLogger('downloadAttachment');
export async function downloadAttachment({
attachment,
options: {
variant = AttachmentVariant.Default,
onSizeUpdate,
abortSignal,
hasMediaBackups,
logId: _logId,
},
dependencies = {
downloadAttachmentFromServer: doDownloadAttachment,
downloadAttachmentFromLocalBackup: doDownloadAttachmentFromLocalBackup,
},
}: {
attachment: AttachmentType;
options: {
variant?: AttachmentVariant;
onSizeUpdate: (totalBytes: number) => void;
abortSignal: AbortSignal;
hasMediaBackups: boolean;
logId: string;
};
dependencies?: {
downloadAttachmentFromServer: typeof doDownloadAttachment;
downloadAttachmentFromLocalBackup: typeof doDownloadAttachmentFromLocalBackup;
};
}): Promise<ReencryptedAttachmentV2> {
const variantForLogging =
variant !== AttachmentVariant.Default ? `[${variant}]` : '';
const logId = `${_logId}${variantForLogging}`;
2021-07-09 12:36:10 -07:00
const { server } = window.textsecure;
if (!server) {
throw new Error('window.textsecure.server is not available!');
}
const isBackupable = hasRequiredInformationForBackup(attachment);
const mightBeOnBackupTierNow = isBackupable && hasMediaBackups;
const mightBeOnBackupTierInTheFuture = isBackupable;
2021-07-09 12:36:10 -07:00
if (wasImportedFromLocalBackup(attachment)) {
log.info(`${logId}: Downloading attachment from local backup`);
try {
const result = await dependencies.downloadAttachmentFromLocalBackup(
attachment,
{ logId }
);
onSizeUpdate(attachment.size);
return result;
} catch (error) {
2025-09-08 16:19:17 -04:00
if (isIncrementalMacVerificationError(error)) {
throw error;
}
// We also just log this error instead of throwing, since we want to still try to
// find it on the backup then transit tiers.
log.error(
`${logId}: error when downloading from local backup; will try backup and transit tier`,
toLogFormat(error)
);
}
}
if (mightBeOnBackupTierNow) {
try {
return await dependencies.downloadAttachmentFromServer(
server,
{ mediaTier: MediaTier.BACKUP, attachment },
{
logId,
onSizeUpdate,
variant,
abortSignal,
}
);
} catch (error) {
2025-09-08 16:19:17 -04:00
if (isIncrementalMacVerificationError(error)) {
throw error;
}
if (isAbortError(error)) {
throw error;
}
2025-09-08 16:19:17 -04:00
const shouldFallbackToTransitTier =
variant !== AttachmentVariant.ThumbnailFromBackup;
if (RemoteConfig.isEnabled('desktop.internalUser')) {
window.reduxActions.toast.showToast({
toastType: ToastType.UnableToDownloadFromBackupTier,
});
}
if (error instanceof HTTPError && error.code === 404) {
// This is an expected occurrence if restoring from a backup before the
// attachment has been moved to the backup tier
log.warn(
`${logId}: attachment not found on backup CDN`,
shouldFallbackToTransitTier ? 'will try transit tier' : ''
);
} else {
// We also just log this error instead of throwing, since we want to still try to
// find it on the attachment tier.
log.error(
`${logId}: error when downloading from backup CDN`,
shouldFallbackToTransitTier ? 'will try transit tier' : '',
toLogFormat(error)
);
}
if (!shouldFallbackToTransitTier) {
throw error;
}
}
}
try {
return await dependencies.downloadAttachmentFromServer(
server,
{ attachment, mediaTier: MediaTier.STANDARD },
{
logId,
onSizeUpdate,
variant,
abortSignal,
}
);
} catch (error) {
2025-09-08 16:19:17 -04:00
if (isIncrementalMacVerificationError(error)) {
throw error;
}
if (isAbortError(error)) {
throw error;
}
2025-09-08 16:19:17 -04:00
if (mightBeOnBackupTierInTheFuture) {
// We don't want to throw the AttachmentPermanentlyUndownloadableError because we
// may just need to wait for this attachment to end up on the backup tier
throw error;
}
// Attachments on the transit tier expire after (message queue length + buffer) days,
// then start returning 404
if (error instanceof HTTPError && error.code === 404) {
throw new AttachmentPermanentlyUndownloadableError(`HTTP ${error.code}`);
} else if (
error instanceof HTTPError &&
// CDN 0 can return 403 which means the same as 404 from other CDNs
error.code === 403 &&
(attachment.cdnNumber == null || attachment.cdnNumber === 0)
) {
throw new AttachmentPermanentlyUndownloadableError(`HTTP ${error.code}`);
} else {
throw error;
}
}
}
2025-09-08 16:19:17 -04:00
export function isIncrementalMacVerificationError(error: unknown): boolean {
return (
error instanceof LibSignalErrorBase &&
error.code === ErrorCode.IncrementalMacVerificationFailed
);
}