Fix some backup export issues

This commit is contained in:
Fedor Indutny
2025-04-24 14:53:42 -07:00
committed by GitHub
parent bebdab211e
commit dcac698631
8 changed files with 75 additions and 23 deletions

View File

@@ -194,6 +194,7 @@ export default {
return {
result: {
totalBytes: 100,
duration: 10000,
stats: {
adHocCalls: 1,
callLinks: 2,

View File

@@ -5,6 +5,7 @@ import React, { useState, useCallback } from 'react';
import type { LocalizerType } from '../types/I18N';
import { toLogFormat } from '../types/errors';
import { formatFileSize } from '../util/formatFileSize';
import { SECOND } from '../util/durations';
import type { ValidationResultType as BackupValidationResultType } from '../services/backups';
import { SettingsRow, SettingsControl } from './PreferencesUtil';
import { Button, ButtonVariant } from './Button';
@@ -38,12 +39,13 @@ export function PreferencesInternal({
if (validationResult != null) {
if ('result' in validationResult) {
const {
result: { totalBytes, stats },
result: { totalBytes, stats, duration },
} = validationResult;
validationElem = (
<div className="Preferences--internal--validate-backup--result">
<p>File size: {formatFileSize(totalBytes)}</p>
<p>Duration: {Math.round(duration / SECOND)}s</p>
<pre>
<code>{JSON.stringify(stats, null, 2)}</code>
</pre>

View File

@@ -543,6 +543,11 @@ export class BackupExportStream extends Readable {
continue;
}
if (status === AdhocCallStatus.Deleted) {
log.info(`backups: Dropping deleted ad-hoc call ${callId.slice(-2)}`);
continue;
}
this.#pushFrame({
adHocCall: {
callId: Long.fromString(callId),
@@ -1029,7 +1034,7 @@ export class BackupExportStream extends Readable {
if (
conversation &&
isGroupV2(conversation.attributes) &&
message.storyReplyContext
(message.storyReplyContext || message.storyReaction)
) {
// We drop group story replies
return undefined;
@@ -1526,10 +1531,14 @@ export class BackupExportStream extends Readable {
simpleUpdate.type = Backups.SimpleChatUpdate.Type.IDENTITY_UPDATE;
if (message.key_changed) {
const target = window.ConversationController.get(message.key_changed);
if (!target) {
throw new Error(
'toChatItemUpdate/keyCahnge: key_changed conversation not found!'
);
}
// This will override authorId on the original chatItem
patch.authorId = this.#getOrPushPrivateRecipient({
id: message.key_changed,
});
patch.authorId = this.#getOrPushPrivateRecipient(target.attributes);
}
updateMessage.simpleUpdate = simpleUpdate;
@@ -2618,7 +2627,6 @@ export class BackupExportStream extends Readable {
| 'bodyAttachment'
| 'bodyRanges'
| 'storyReaction'
| 'storyReplyContext'
| 'received_at'
| 'reactions'
>;

View File

@@ -3283,7 +3283,7 @@ export class BackupImportStream extends Writable {
case Type.IDENTITY_UPDATE:
return {
type: 'keychange',
key_changed: isGroup(conversation) ? author?.id : undefined,
key_changed: isGroup(conversation) ? author?.serviceId : undefined,
};
case Type.IDENTITY_VERIFIED:
strictAssert(author != null, 'IDENTITY_VERIFIED must have an author');

View File

@@ -100,6 +100,7 @@ export type ImportOptionsType = Readonly<{
export type ExportResultType = Readonly<{
totalBytes: number;
duration: number;
stats: Readonly<StatsType>;
}>;
@@ -767,6 +768,7 @@ export class BackupsService {
log.info('exportBackup: starting...');
this.#isRunning = 'export';
const start = Date.now();
try {
// TODO (DESKTOP-7168): Update mock-server to support this endpoint
if (window.SignalCI || backupType === BackupType.TestOnlyPlaintext) {
@@ -815,7 +817,8 @@ export class BackupsService {
throw missingCaseError(backupType);
}
return { totalBytes, stats: recordStream.getStats() };
const duration = Date.now() - start;
return { totalBytes, stats: recordStream.getStats(), duration };
} finally {
log.info('exportBackup: finished...');
this.#isRunning = false;

View File

@@ -40,7 +40,6 @@ import { bytesToUuid } from '../../../util/uuidToBytes';
import { createName } from '../../../util/attachmentPath';
import { ensureAttachmentIsReencryptable } from '../../../util/ensureAttachmentIsReencryptable';
import type { ReencryptionInfo } from '../../../AttachmentCrypto';
import { dropZero } from '../../../util/dropZero';
export function convertFilePointerToAttachment(
filePointer: Backups.FilePointer,
@@ -70,13 +69,16 @@ export function convertFilePointerToAttachment(
fileName: fileName ?? undefined,
caption: caption ?? undefined,
blurHash: blurHash ?? undefined,
incrementalMac: incrementalMac?.length
? Bytes.toBase64(incrementalMac)
: undefined,
chunkSize: dropZero(incrementalMacChunkSize),
incrementalMac: undefined,
chunkSize: undefined,
downloadPath: doCreateName(),
};
if (incrementalMac?.length && incrementalMacChunkSize) {
commonProps.incrementalMac = Bytes.toBase64(incrementalMac);
commonProps.chunkSize = incrementalMacChunkSize;
}
if (attachmentLocator) {
const { cdnKey, cdnNumber, key, digest, uploadTimestamp, size } =
attachmentLocator;
@@ -180,17 +182,22 @@ export async function getFilePointerForAttachment({
}> {
const filePointerRootProps = new Backups.FilePointer({
contentType: attachment.contentType,
// Resilience to invalid data in the database from internal testing
incrementalMac:
typeof attachment.incrementalMac === 'string'
? Bytes.fromBase64(attachment.incrementalMac)
: undefined,
incrementalMacChunkSize: dropZero(attachment.chunkSize),
fileName: attachment.fileName,
width: attachment.width,
height: attachment.height,
caption: attachment.caption,
blurHash: attachment.blurHash,
// Resilience to invalid data in the database from internal testing
...(typeof attachment.incrementalMac === 'string' && attachment.chunkSize
? {
incrementalMac: Bytes.fromBase64(attachment.incrementalMac),
incrementalMacChunkSize: attachment.chunkSize,
}
: {
incrementalMac: undefined,
incrementalMacChunkSize: undefined,
}),
});
const logId = `getFilePointerForAttachment(${redactGenericText(
attachment.digest ?? ''

View File

@@ -65,7 +65,6 @@ function sortAndNormalize(
changedId,
conversationId,
editHistory,
key_changed: keyChanged,
reactions,
sendStateByConversationId,
verifiedChanged,
@@ -132,7 +131,6 @@ function sortAndNormalize(
};
}),
changedId: mapConvoId(changedId),
key_changed: mapConvoId(keyChanged),
verifiedChanged: mapConvoId(verifiedChanged),
sendStateByConverationId: mapSendState(sendStateByConversationId),
editHistory: editHistory?.map(history => {

View File

@@ -112,13 +112,13 @@ describe('backup/non-bubble messages', () => {
]);
});
it('roundtrips IDENTITY_CHANGE update in groups', async () => {
it('roundtrips ACI IDENTITY_CHANGE update in groups', async () => {
await symmetricRoundtripHarness([
{
conversationId: group.id,
id: generateGuid(),
type: 'keychange',
key_changed: contactA.id,
key_changed: contactA.getAci(),
received_at: 1,
sent_at: 1,
timestamp: 1,
@@ -129,6 +129,39 @@ describe('backup/non-bubble messages', () => {
]);
});
it('roundtrips id IDENTITY_CHANGE update in groups', async () => {
await asymmetricRoundtripHarness(
[
{
conversationId: group.id,
id: generateGuid(),
type: 'keychange',
key_changed: contactA.id,
received_at: 1,
sent_at: 1,
timestamp: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: CONTACT_A,
},
],
[
{
conversationId: group.id,
id: generateGuid(),
type: 'keychange',
key_changed: contactA.getAci(),
received_at: 1,
sent_at: 1,
timestamp: 1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
sourceServiceId: CONTACT_A,
},
]
);
});
it('roundtrips IDENTITY_DEFAULT simple update', async () => {
await symmetricRoundtripHarness([
{