Path: blob/main/SignalServiceKit/Backups/Archiving/Archivers/StickerPack/BackupArchiveStickerPackArchiver.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import GRDB
public extension BackupArchive {
/// An identifier for a ``BackupProto_StickerPack`` backup frame.
struct StickerPackId: BackupArchive.LoggableId {
let value: Data
init(_ value: Data) {
self.value = value
}
// MARK: BackupArchive.LoggableId
public var typeLogString: String { "BackupProto_StickPack" }
public var idLogString: String {
/// Since sticker pack IDs are a cross-client identifier, we don't
/// want to log them directly.
return "\(value.hashValue)"
}
}
}
// MARK: -
public class BackupArchiveStickerPackArchiver: BackupArchiveProtoStreamWriter {
typealias StickerPackId = BackupArchive.StickerPackId
typealias ArchiveMultiFrameResult = BackupArchive.ArchiveMultiFrameResult<StickerPackId>
typealias ArchiveFrameError = BackupArchive.ArchiveFrameError<StickerPackId>
typealias RestoreFrameResult = BackupArchive.RestoreFrameResult<StickerPackId>
private let backupStickerPackDownloadStore: BackupStickerPackDownloadStore
init(
backupStickerPackDownloadStore: BackupStickerPackDownloadStore,
) {
self.backupStickerPackDownloadStore = backupStickerPackDownloadStore
}
// MARK: -
/// Archive all ``StickerPack``s (they map to ``BackupProto_StickerPack``).
///
/// - Returns: ``ArchiveMultiFrameResult.success`` if all frames were written without error, or either
/// partial or complete failure otherwise.
/// How to handle ``ArchiveMultiFrameResult.partialSuccess`` is up to the caller,
/// but typically an error will be shown to the user, but the backup will be allowed to proceed.
/// ``ArchiveMultiFrameResult.completeFailure``, on the other hand, will stop the entire backup,
/// and should be used if some critical or category-wide failure occurs.
func archiveStickerPacks(
stream: BackupArchiveProtoOutputStream,
context: BackupArchive.ArchivingContext,
) throws(CancellationError) -> ArchiveMultiFrameResult {
var errors = [ArchiveFrameError]()
var handledPacks = Set<Data>()
func archiveInstalledStickerPack(
_ installedStickerPack: StickerPackRecord,
_ frameBencher: BackupArchive.Bencher.FrameBencher,
) {
autoreleasepool {
guard !handledPacks.contains(installedStickerPack.packId) else { return }
let maybeError: ArchiveFrameError? = Self.writeFrameToStream(
stream,
objectId: StickerPackId(installedStickerPack.packId),
frameBencher: frameBencher,
) {
var stickerPack = BackupProto_StickerPack()
stickerPack.packID = installedStickerPack.packId
stickerPack.packKey = installedStickerPack.packKey
var frame = BackupProto_Frame()
frame.item = .stickerPack(stickerPack)
return frame
}
if let maybeError {
errors.append(maybeError)
} else {
handledPacks.insert(installedStickerPack.packId)
}
}
}
func enumerateStickerPackRecord(tx: DBReadTransaction, block: (StickerPackRecord) throws -> Void) throws {
let cursor = try StickerPackRecord
.filter(Column(StickerPackRecord.CodingKeys.isInstalled) == true)
.fetchCursor(tx.database)
while let stickerPack = try cursor.next() {
try block(stickerPack)
}
}
// Iterate over the installed sticker packs
do {
try context.bencher.wrapEnumeration(
enumerateStickerPackRecord(tx:block:),
tx: context.tx,
) { stickerPack, frameBencher in
try Task.checkCancellation()
archiveInstalledStickerPack(stickerPack, frameBencher)
}
} catch let error as CancellationError {
throw error
} catch {
return .completeFailure(.fatalArchiveError(.stickerPackIteratorError(error)))
}
// Iterate over any restored sticker packs that have yet to be downloaded via StickerManager.
do {
try context.bencher.wrapEnumeration(
backupStickerPackDownloadStore.iterateAllEnqueued(tx:block:),
tx: context.tx,
) { record, frameBencher in
try Task.checkCancellation()
autoreleasepool {
guard !handledPacks.contains(record.packId) else { return }
let maybeError: ArchiveFrameError? = Self.writeFrameToStream(
stream,
objectId: StickerPackId(record.packId),
frameBencher: frameBencher,
) {
var stickerPack = BackupProto_StickerPack()
stickerPack.packID = record.packId
stickerPack.packKey = record.packKey
var frame = BackupProto_Frame()
frame.item = .stickerPack(stickerPack)
return frame
}
if let maybeError {
errors.append(maybeError)
} else {
handledPacks.insert(record.packId)
}
}
}
} catch let error as CancellationError {
throw error
} catch {
return .completeFailure(.fatalArchiveError(.stickerPackIteratorError(error)))
}
if errors.count > 0 {
return .partialSuccess(errors)
} else {
return .success
}
}
// MARK: -
/// Restore a single ``BackupProto_StickerPack`` frame.
///
/// - Returns: ``RestoreFrameResult.success`` if the frame was restored without error.
/// How to handle ``RestoreFrameResult.failure`` is up to the caller,
/// but typically an error will be shown to the user, but the restore will be allowed to proceed.
func restore(
_ stickerPack: BackupProto_StickerPack,
context: BackupArchive.RestoringContext,
) -> RestoreFrameResult {
do {
try backupStickerPackDownloadStore.enqueue(
packId: stickerPack.packID,
packKey: stickerPack.packKey,
tx: context.tx,
)
} catch {
return .failure([.restoreFrameError(.databaseInsertionFailed(error), StickerPackId(stickerPack.packID))])
}
return .success
}
}