Generate mediaName for backed-up attachments
This commit is contained in:
parent
db623d13b2
commit
cf381cd46c
4 changed files with 171 additions and 2 deletions
|
@ -525,7 +525,7 @@ export async function addPlaintextHashToAttachment(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPlaintextHashForAttachmentOnDisk(
|
export async function getPlaintextHashForAttachmentOnDisk(
|
||||||
absolutePath: string
|
absolutePath: string
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
let readFd;
|
let readFd;
|
||||||
|
|
102
ts/test-electron/backup/backup_attachments_test.ts
Normal file
102
ts/test-electron/backup/backup_attachments_test.ts
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import * as sinon from 'sinon';
|
||||||
|
|
||||||
|
import { getMediaNameForBackup } from '../../util/attachments/getMediaNameForBackup';
|
||||||
|
import { IMAGE_PNG } from '../../types/MIME';
|
||||||
|
import { sha256 } from '../../Crypto';
|
||||||
|
import { DAY } from '../../util/durations';
|
||||||
|
|
||||||
|
describe('getMediaNameForBackup', () => {
|
||||||
|
const TEST_HASH = sha256(Buffer.from('testattachmentdata'));
|
||||||
|
const TEST_HASH_BASE_64 =
|
||||||
|
// calculated as Buffer.from(TEST_HASH).toString('base64')
|
||||||
|
'ds5/U14lB2ziO90B7MldFTJUQdyw4qQ9y6Gnt9fmHL0=';
|
||||||
|
|
||||||
|
afterEach(function (this: Mocha.Context) {
|
||||||
|
sinon.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return base64 encoded plaintextHash if it's already been calculated", async () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
await getMediaNameForBackup(
|
||||||
|
{
|
||||||
|
contentType: IMAGE_PNG,
|
||||||
|
size: 100,
|
||||||
|
plaintextHash: Buffer.from(TEST_HASH).toString('hex'),
|
||||||
|
},
|
||||||
|
'senderAci',
|
||||||
|
Date.now()
|
||||||
|
),
|
||||||
|
TEST_HASH_BASE_64
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate hash from file on disk if plaintextHash has not yet been calculated', async () => {
|
||||||
|
const stubbedGetHashFromDisk = sinon
|
||||||
|
.stub()
|
||||||
|
.callsFake(async (_path: string) =>
|
||||||
|
Buffer.from(TEST_HASH).toString('hex')
|
||||||
|
);
|
||||||
|
|
||||||
|
const mediaName = await getMediaNameForBackup(
|
||||||
|
{
|
||||||
|
contentType: IMAGE_PNG,
|
||||||
|
size: 100,
|
||||||
|
path: 'path/to/file',
|
||||||
|
},
|
||||||
|
'senderAci',
|
||||||
|
Date.now(),
|
||||||
|
{ getPlaintextHashForAttachmentOnDisk: stubbedGetHashFromDisk }
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(stubbedGetHashFromDisk.callCount, 1);
|
||||||
|
assert.strictEqual(mediaName, TEST_HASH_BASE_64);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return temporary identifier if attachment is undownloaded but in attachment tier', async () => {
|
||||||
|
const mediaName = await getMediaNameForBackup(
|
||||||
|
{
|
||||||
|
contentType: IMAGE_PNG,
|
||||||
|
size: 100,
|
||||||
|
cdnKey: 'cdnKey',
|
||||||
|
},
|
||||||
|
'senderAci',
|
||||||
|
Date.now()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(mediaName, 'senderAci_cdnKey');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return temporary identifier if undownloaded attachment has temporary error', async () => {
|
||||||
|
const mediaName = await getMediaNameForBackup(
|
||||||
|
{
|
||||||
|
contentType: IMAGE_PNG,
|
||||||
|
size: 100,
|
||||||
|
cdnKey: 'cdnKey',
|
||||||
|
error: true,
|
||||||
|
key: 'attachmentkey',
|
||||||
|
},
|
||||||
|
'senderAci',
|
||||||
|
Date.now()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(mediaName, 'senderAci_cdnKey');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined if attachment is too old to be in attachment tier', async () => {
|
||||||
|
const mediaName = await getMediaNameForBackup(
|
||||||
|
{
|
||||||
|
contentType: IMAGE_PNG,
|
||||||
|
size: 100,
|
||||||
|
cdnKey: 'cdnKey',
|
||||||
|
},
|
||||||
|
'senderAci',
|
||||||
|
Date.now() - 31 * DAY
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(mediaName, undefined);
|
||||||
|
});
|
||||||
|
});
|
|
@ -28,7 +28,7 @@ import type { ProcessedAttachment } from './Types.d';
|
||||||
import type { WebAPIType } from './WebAPI';
|
import type { WebAPIType } from './WebAPI';
|
||||||
import { createName, getRelativePath } from '../windows/attachments';
|
import { createName, getRelativePath } from '../windows/attachments';
|
||||||
|
|
||||||
function getCdn(attachment: ProcessedAttachment) {
|
export function getCdn(attachment: ProcessedAttachment): string {
|
||||||
const { cdnId, cdnKey } = attachment;
|
const { cdnId, cdnKey } = attachment;
|
||||||
const cdn = cdnId || cdnKey;
|
const cdn = cdnId || cdnKey;
|
||||||
strictAssert(cdn, 'Attachment was missing cdnId or cdnKey');
|
strictAssert(cdn, 'Attachment was missing cdnId or cdnKey');
|
||||||
|
|
67
ts/util/attachments/getMediaNameForBackup.ts
Normal file
67
ts/util/attachments/getMediaNameForBackup.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { getPlaintextHashForAttachmentOnDisk } from '../../AttachmentCrypto';
|
||||||
|
import type { AttachmentType } from '../../types/Attachment';
|
||||||
|
import { DAY } from '../durations';
|
||||||
|
import * as log from '../../logging/log';
|
||||||
|
import { isOlderThan } from '../timestamp';
|
||||||
|
import { getCdn } from '../../textsecure/downloadAttachment';
|
||||||
|
import * as Bytes from '../../Bytes';
|
||||||
|
|
||||||
|
const TIME_IN_ATTACHMENT_TIER = 30 * DAY;
|
||||||
|
|
||||||
|
// We store the plaintext hash as a hex string, but the mediaName should be
|
||||||
|
// the base64 encoded version.
|
||||||
|
function convertHexStringToBase64(hexString: string): string {
|
||||||
|
return Bytes.toBase64(Bytes.fromHex(hexString));
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetMediaNameDependenciesType = {
|
||||||
|
getPlaintextHashForAttachmentOnDisk: (
|
||||||
|
path: string
|
||||||
|
) => Promise<string | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getMediaNameForBackup(
|
||||||
|
attachment: AttachmentType,
|
||||||
|
senderAci: string,
|
||||||
|
messageTimestamp: number,
|
||||||
|
// allow optional dependency injection for testing
|
||||||
|
dependencies: GetMediaNameDependenciesType = {
|
||||||
|
getPlaintextHashForAttachmentOnDisk,
|
||||||
|
}
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
if (attachment.plaintextHash) {
|
||||||
|
return convertHexStringToBase64(attachment.plaintextHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachment.path) {
|
||||||
|
const hashFromFileOnDisk =
|
||||||
|
await dependencies.getPlaintextHashForAttachmentOnDisk(
|
||||||
|
window.Signal.Migrations.getAbsoluteAttachmentPath(attachment.path)
|
||||||
|
);
|
||||||
|
if (!hashFromFileOnDisk) {
|
||||||
|
log.error(
|
||||||
|
'getMediaNameForBackup: no hash from attachment on disk (maybe it is empty?)'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return convertHexStringToBase64(hashFromFileOnDisk);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cdnKey = getCdn(attachment);
|
||||||
|
if (!cdnKey) {
|
||||||
|
log.error('getMediaNameForBackup: attachment has no cdnKey');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOlderThan(messageTimestamp, TIME_IN_ATTACHMENT_TIER)) {
|
||||||
|
log.error(
|
||||||
|
"getMediaNameForBackup: attachment is not downloaded but is too old; it's no longer in attachment tier."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${senderAci}_${cdnKey}`;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue