// Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { type Readable } from 'node:stream'; import { strictAssert } from '../../util/assert'; import type { WebAPIType, AttachmentUploadFormResponseType, GetBackupInfoResponseType, BackupMediaItemType, BackupMediaBatchResponseType, BackupListMediaResponseType, } from '../../textsecure/WebAPI'; import type { BackupCredentials } from './credentials'; import { uploadFile } from '../../util/uploadAttachment'; export type DownloadOptionsType = Readonly<{ downloadOffset: number; onProgress: (currentBytes: number, totalBytes: number) => void; abortSignal?: AbortSignal; }>; export class BackupAPI { private cachedBackupInfo: GetBackupInfoResponseType | undefined; constructor(private credentials: BackupCredentials) {} public async refresh(): Promise { await this.server.refreshBackup( await this.credentials.getHeadersForToday() ); } public async getInfo(): Promise { const backupInfo = await this.server.getBackupInfo( await this.credentials.getHeadersForToday() ); this.cachedBackupInfo = backupInfo; return backupInfo; } private async getCachedInfo(): Promise { if (this.cachedBackupInfo) { return this.cachedBackupInfo; } return this.getInfo(); } public async getMediaDir(): Promise { return (await this.getCachedInfo()).mediaDir; } public async getBackupDir(): Promise { return (await this.getCachedInfo())?.backupDir; } // Backup name will change whenever a new backup is created, so we don't want to cache // it public async getBackupName(): Promise { return (await this.getInfo()).backupName; } public async upload(filePath: string, fileSize: number): Promise { const form = await this.server.getBackupUploadForm( await this.credentials.getHeadersForToday() ); await uploadFile({ absoluteCiphertextPath: filePath, ciphertextFileSize: fileSize, uploadForm: form, }); } public async download({ downloadOffset, onProgress, abortSignal, }: DownloadOptionsType): Promise { const { cdn, backupDir, backupName } = await this.getInfo(); const { headers } = await this.credentials.getCDNReadCredentials(cdn); return this.server.getBackupStream({ cdn, backupDir, backupName, headers, downloadOffset, onProgress, abortSignal, }); } public async downloadEphemeral({ downloadOffset, onProgress, abortSignal, }: DownloadOptionsType): Promise { const { cdn, key } = await this.server.getTransferArchive({ abortSignal, }); return this.server.getEphemeralBackupStream({ cdn, key, downloadOffset, onProgress, abortSignal, }); } public async getMediaUploadForm(): Promise { return this.server.getBackupMediaUploadForm( await this.credentials.getHeadersForToday() ); } public async backupMediaBatch( items: ReadonlyArray ): Promise { return this.server.backupMediaBatch({ headers: await this.credentials.getHeadersForToday(), items, }); } public async listMedia({ cursor, limit, }: { cursor?: string; limit: number; }): Promise { return this.server.backupListMedia({ headers: await this.credentials.getHeadersForToday(), cursor, limit, }); } public clearCache(): void { this.cachedBackupInfo = undefined; } private get server(): WebAPIType { const { server } = window.textsecure; strictAssert(server, 'server not available'); return server; } }