Compute mediaName dynamically
This commit is contained in:
parent
3914c811be
commit
221043a998
@ -856,6 +856,7 @@
|
||||
50D839512F916A3700EE009A /* MessageRequestDecliner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D839502F916A3700EE009A /* MessageRequestDecliner.swift */; };
|
||||
50D8796A2A16D2C20031345D /* MessageLoaderBatchTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D879692A16D2C20031345D /* MessageLoaderBatchTest.swift */; };
|
||||
50D9CD8D2C52D78000273D6C /* StoryRecipientManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D9CD8C2C52D78000273D6C /* StoryRecipientManager.swift */; };
|
||||
50DAF7E02FD87BEC00BE7430 /* OrphanedBackupAttachmentTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DAF7DF2FD87BEC00BE7430 /* OrphanedBackupAttachmentTest.swift */; };
|
||||
50DCCBFA2F1817280024D124 /* DisappearingMessagesConfigurationMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DCCBF92F1817280024D124 /* DisappearingMessagesConfigurationMessage.swift */; };
|
||||
50DCCBFC2F181A790024D124 /* ProfileKeyMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DCCBFB2F181A790024D124 /* ProfileKeyMessage.swift */; };
|
||||
50DCCBFE2F1820600024D124 /* OutgoingSenderKeyDistributionMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DCCBFD2F1820600024D124 /* OutgoingSenderKeyDistributionMessage.swift */; };
|
||||
@ -5121,6 +5122,7 @@
|
||||
50D839502F916A3700EE009A /* MessageRequestDecliner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestDecliner.swift; sourceTree = "<group>"; };
|
||||
50D879692A16D2C20031345D /* MessageLoaderBatchTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLoaderBatchTest.swift; sourceTree = "<group>"; };
|
||||
50D9CD8C2C52D78000273D6C /* StoryRecipientManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryRecipientManager.swift; sourceTree = "<group>"; };
|
||||
50DAF7DF2FD87BEC00BE7430 /* OrphanedBackupAttachmentTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrphanedBackupAttachmentTest.swift; sourceTree = "<group>"; };
|
||||
50DCCBF92F1817280024D124 /* DisappearingMessagesConfigurationMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisappearingMessagesConfigurationMessage.swift; sourceTree = "<group>"; };
|
||||
50DCCBFB2F181A790024D124 /* ProfileKeyMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileKeyMessage.swift; sourceTree = "<group>"; };
|
||||
50DCCBFD2F1820600024D124 /* OutgoingSenderKeyDistributionMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingSenderKeyDistributionMessage.swift; sourceTree = "<group>"; };
|
||||
@ -9898,6 +9900,22 @@
|
||||
path = Debugging;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
50DAF7E12FD87BFD00BE7430 /* Backups */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50DAF7E22FD87C7000BE7430 /* Attachments */,
|
||||
);
|
||||
path = Backups;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
50DAF7E22FD87C7000BE7430 /* Attachments */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50DAF7DF2FD87BEC00BE7430 /* OrphanedBackupAttachmentTest.swift */,
|
||||
);
|
||||
path = Attachments;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
50E0198E2CC2491A0063EA48 /* Concurrency */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -14633,6 +14651,7 @@
|
||||
F94261FF289B1B5400460798 /* Account */,
|
||||
D92EFDED2F69B9D00031D257 /* AttachmentBackfill */,
|
||||
50ED28002F0EDAFB00E57C54 /* Attachments */,
|
||||
50DAF7E12FD87BFD00BE7430 /* Backups */,
|
||||
F945FE4B298481D800C835C7 /* Calls */,
|
||||
D985D86229B91C2B0087C90C /* ChangePhoneNumber */,
|
||||
50E0198E2CC2491A0063EA48 /* Concurrency */,
|
||||
@ -20171,6 +20190,7 @@
|
||||
D979CC3A2AD3964E006AAC49 /* Numbers+Random.swift in Sources */,
|
||||
D95E149D2E3D22FD00B5B70B /* ObjectRetainerTest.swift in Sources */,
|
||||
663D02DF2C069AB600350632 /* OrphanedAttachmentCleanerTest.swift in Sources */,
|
||||
50DAF7E02FD87BEC00BE7430 /* OrphanedBackupAttachmentTest.swift in Sources */,
|
||||
D9AA37A02A86E0910088EFFB /* OutgoingCallEventSyncMessageTest.swift in Sources */,
|
||||
D925C7BB2B7BEC0F00AC73B0 /* OutgoingCallLogEventSyncMessageTest.swift in Sources */,
|
||||
D9D3216A2A8AC9B0004FC110 /* OutgoingGroupCallUpdateMessageTest.swift in Sources */,
|
||||
|
||||
@ -394,7 +394,7 @@ class BackupListMediaManagerImpl: BackupListMediaManager {
|
||||
|
||||
var query = Attachment.Record
|
||||
.order(Column(Attachment.Record.CodingKeys.sqliteId).asc)
|
||||
.filter(Column(Attachment.Record.CodingKeys.mediaName) != nil)
|
||||
.filter(Column(Attachment.Record.CodingKeys.plaintextHash) != nil)
|
||||
|
||||
if let id = txContext.lastEnumeratedAttachmentId {
|
||||
query = query
|
||||
@ -761,7 +761,6 @@ class BackupListMediaManagerImpl: BackupListMediaManager {
|
||||
attachment,
|
||||
listedMedia: matchedListedMedia,
|
||||
isThumbnail: isThumbnail,
|
||||
fullsizeMediaName: fullsizeMediaName,
|
||||
uploadEraAtStartOfListMedia: uploadEraAtStartOfListMedia,
|
||||
currentBackupPlan: currentBackupPlan,
|
||||
remoteConfig: remoteConfig,
|
||||
@ -950,7 +949,6 @@ class BackupListMediaManagerImpl: BackupListMediaManager {
|
||||
_ attachment: Attachment,
|
||||
listedMedia: ListedBackupMediaObject,
|
||||
isThumbnail: Bool,
|
||||
fullsizeMediaName: String,
|
||||
uploadEraAtStartOfListMedia: String,
|
||||
currentBackupPlan: BackupPlan,
|
||||
remoteConfig: RemoteConfig,
|
||||
@ -962,7 +960,6 @@ class BackupListMediaManagerImpl: BackupListMediaManager {
|
||||
from: listedMedia,
|
||||
isThumbnail: isThumbnail,
|
||||
uploadEraAtStartOfListMedia: uploadEraAtStartOfListMedia,
|
||||
fullsizeMediaName: fullsizeMediaName,
|
||||
tx: tx,
|
||||
)
|
||||
|
||||
@ -1015,7 +1012,6 @@ class BackupListMediaManagerImpl: BackupListMediaManager {
|
||||
from listedMedia: ListedBackupMediaObject,
|
||||
isThumbnail: Bool,
|
||||
uploadEraAtStartOfListMedia: String,
|
||||
fullsizeMediaName: String,
|
||||
tx: DBWriteTransaction,
|
||||
) -> Bool {
|
||||
if isThumbnail {
|
||||
@ -1027,7 +1023,6 @@ class BackupListMediaManagerImpl: BackupListMediaManager {
|
||||
uploadEra: uploadEraAtStartOfListMedia,
|
||||
lastDownloadAttemptTimestamp: nil,
|
||||
),
|
||||
mediaName: fullsizeMediaName,
|
||||
tx: tx,
|
||||
)
|
||||
return true
|
||||
@ -1068,7 +1063,6 @@ class BackupListMediaManagerImpl: BackupListMediaManager {
|
||||
uploadEra: uploadEraAtStartOfListMedia,
|
||||
lastDownloadAttemptTimestamp: nil,
|
||||
),
|
||||
mediaName: fullsizeMediaName,
|
||||
tx: tx,
|
||||
)
|
||||
return true
|
||||
|
||||
@ -91,12 +91,14 @@ public class BackupListMediaManagerTests {
|
||||
// Case 1: Attachment exists locally but not on CDN
|
||||
let localOnlyIds = await db.awaitableWrite { tx in
|
||||
return (0..<numAttachmentsPerCase).map { _ in
|
||||
let plaintextHash = Randomness.generateRandomBytes(32)
|
||||
return insertAttachment(
|
||||
mediaName: UUID().uuidString,
|
||||
plaintextHash: plaintextHash,
|
||||
encryptionKey: .generate(),
|
||||
mediaTierInfo: .init(
|
||||
cdnNumber: 1,
|
||||
unencryptedByteCount: 100,
|
||||
plaintextHash: UUID().data,
|
||||
plaintextHash: plaintextHash,
|
||||
incrementalMacInfo: nil,
|
||||
uploadEra: localUploadEra,
|
||||
lastDownloadAttemptTimestamp: nil,
|
||||
@ -111,7 +113,7 @@ public class BackupListMediaManagerTests {
|
||||
let remoteOnlyCdnNumberMedia = (0..<numAttachmentsPerCase).map { _ in
|
||||
return BackupArchive.Response.StoredMedia(
|
||||
cdn: orphanCdnNumber,
|
||||
mediaId: UUID().uuidString,
|
||||
mediaId: Randomness.generateRandomBytes(15).hexadecimalString,
|
||||
objectLength: 100,
|
||||
)
|
||||
}
|
||||
@ -124,7 +126,12 @@ public class BackupListMediaManagerTests {
|
||||
var discoveredCdnNumberMedia = [BackupArchive.Response.StoredMedia]()
|
||||
let discoveredCdnNumberIds = await db.awaitableWrite { tx in
|
||||
return (0..<numAttachmentsPerCase).map { _ in
|
||||
let mediaName = UUID().uuidString
|
||||
let plaintextHash = Randomness.generateRandomBytes(32)
|
||||
let encryptionKey = AttachmentKey.generate()
|
||||
let mediaName = Attachment.mediaName(
|
||||
plaintextHash: plaintextHash,
|
||||
encryptionKey: encryptionKey.combinedKey,
|
||||
)
|
||||
let fullsizeMediaId = try! mediaRootBackupKey.deriveMediaId(mediaName)
|
||||
let thumbnailMediaId = try! mediaRootBackupKey.deriveMediaId(AttachmentBackupThumbnail.thumbnailMediaName(fullsizeMediaName: mediaName))
|
||||
for mediaId in [fullsizeMediaId, thumbnailMediaId] {
|
||||
@ -135,7 +142,8 @@ public class BackupListMediaManagerTests {
|
||||
))
|
||||
}
|
||||
return insertAttachment(
|
||||
mediaName: mediaName,
|
||||
plaintextHash: plaintextHash,
|
||||
encryptionKey: encryptionKey,
|
||||
mediaTierInfo: nil,
|
||||
tx: tx,
|
||||
)
|
||||
@ -147,7 +155,12 @@ public class BackupListMediaManagerTests {
|
||||
var matchingCdnNumberMedia = [BackupArchive.Response.StoredMedia]()
|
||||
let matchingCdnNumberIds = await db.awaitableWrite { tx in
|
||||
return (0..<numAttachmentsPerCase).map { _ in
|
||||
let mediaName = UUID().uuidString
|
||||
let plaintextHash = Randomness.generateRandomBytes(32)
|
||||
let encryptionKey = AttachmentKey.generate()
|
||||
let mediaName = Attachment.mediaName(
|
||||
plaintextHash: plaintextHash,
|
||||
encryptionKey: encryptionKey.combinedKey,
|
||||
)
|
||||
let fullsizeMediaId = try! mediaRootBackupKey.deriveMediaId(mediaName)
|
||||
let thumbnailMediaId = try! mediaRootBackupKey.deriveMediaId(AttachmentBackupThumbnail.thumbnailMediaName(fullsizeMediaName: mediaName))
|
||||
for mediaId in [fullsizeMediaId, thumbnailMediaId] {
|
||||
@ -163,11 +176,12 @@ public class BackupListMediaManagerTests {
|
||||
))
|
||||
}
|
||||
return insertAttachment(
|
||||
mediaName: mediaName,
|
||||
plaintextHash: plaintextHash,
|
||||
encryptionKey: encryptionKey,
|
||||
mediaTierInfo: .init(
|
||||
cdnNumber: matchingCdnNumber,
|
||||
unencryptedByteCount: 100,
|
||||
plaintextHash: UUID().data,
|
||||
plaintextHash: plaintextHash,
|
||||
incrementalMacInfo: nil,
|
||||
uploadEra: localUploadEra,
|
||||
lastDownloadAttemptTimestamp: nil,
|
||||
@ -181,7 +195,12 @@ public class BackupListMediaManagerTests {
|
||||
var nonMatchingCdnNumberMedia = [BackupArchive.Response.StoredMedia]()
|
||||
let nonMatchingCdnNumberIds = await db.awaitableWrite { tx in
|
||||
return (0..<numAttachmentsPerCase).map { _ in
|
||||
let mediaName = UUID().uuidString
|
||||
let plaintextHash = Randomness.generateRandomBytes(32)
|
||||
let encryptionKey = AttachmentKey.generate()
|
||||
let mediaName = Attachment.mediaName(
|
||||
plaintextHash: plaintextHash,
|
||||
encryptionKey: encryptionKey.combinedKey,
|
||||
)
|
||||
let fullsizeMediaId = try! mediaRootBackupKey.deriveMediaId(mediaName)
|
||||
let thumbnailMediaId = try! mediaRootBackupKey.deriveMediaId(AttachmentBackupThumbnail.thumbnailMediaName(fullsizeMediaName: mediaName))
|
||||
for mediaId in [fullsizeMediaId, thumbnailMediaId] {
|
||||
@ -199,11 +218,12 @@ public class BackupListMediaManagerTests {
|
||||
))
|
||||
}
|
||||
return insertAttachment(
|
||||
mediaName: mediaName,
|
||||
plaintextHash: plaintextHash,
|
||||
encryptionKey: encryptionKey,
|
||||
mediaTierInfo: .init(
|
||||
cdnNumber: matchingCdnNumber,
|
||||
unencryptedByteCount: 100,
|
||||
plaintextHash: UUID().data,
|
||||
plaintextHash: plaintextHash,
|
||||
incrementalMacInfo: nil,
|
||||
uploadEra: localUploadEra,
|
||||
lastDownloadAttemptTimestamp: nil,
|
||||
@ -329,14 +349,20 @@ public class BackupListMediaManagerTests {
|
||||
typealias Attachment = SignalServiceKit.Attachment
|
||||
|
||||
private func insertAttachment(
|
||||
mediaName: String,
|
||||
plaintextHash: Data,
|
||||
encryptionKey: AttachmentKey,
|
||||
mediaTierInfo: Attachment.MediaTierInfo?,
|
||||
tx: DBWriteTransaction,
|
||||
) -> Attachment.IDType {
|
||||
owsPrecondition(mediaTierInfo == nil || mediaTierInfo!.plaintextHash == plaintextHash)
|
||||
|
||||
let thread = TSThread(uniqueId: UUID().uuidString)
|
||||
try! thread.insert(tx.database)
|
||||
|
||||
var attachmentRecord = Attachment.Record.mockStream(mediaName: mediaName)
|
||||
var attachmentRecord = Attachment.Record.mockStream(
|
||||
encryptionKey: encryptionKey,
|
||||
plaintextHash: plaintextHash,
|
||||
)
|
||||
try! attachmentRecord.insert(tx.database)
|
||||
|
||||
if let mediaTierInfo {
|
||||
|
||||
@ -58,7 +58,11 @@ public class Attachment {
|
||||
|
||||
/// MediaName used for backups (but assigned even if backups disabled).
|
||||
/// Nonnull if downloaded OR if restored from a backup.
|
||||
public var mediaName: String?
|
||||
public var mediaName: String? {
|
||||
return self.plaintextHash.map {
|
||||
return Attachment.mediaName(plaintextHash: $0, encryptionKey: self.encryptionKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// If null, the resource has not been uploaded to the media tier.
|
||||
public var mediaTierInfo: MediaTierInfo?
|
||||
@ -146,7 +150,7 @@ public class Attachment {
|
||||
init(pendingAttachment: PendingAttachment) {
|
||||
self.init(
|
||||
plaintextHash: pendingAttachment.plaintextHash,
|
||||
mediaName: pendingAttachment.mediaName,
|
||||
encryptionKey: pendingAttachment.encryptionKey,
|
||||
encryptedByteCount: pendingAttachment.encryptedByteCount,
|
||||
unencryptedByteCount: pendingAttachment.unencryptedByteCount,
|
||||
cachedMediaSizePixels: pendingAttachment.mediaPixelSize,
|
||||
@ -161,7 +165,7 @@ public class Attachment {
|
||||
|
||||
init(
|
||||
plaintextHash: Data,
|
||||
mediaName: String,
|
||||
encryptionKey: Data,
|
||||
encryptedByteCount: UInt32,
|
||||
unencryptedByteCount: UInt32,
|
||||
cachedMediaSizePixels: CGSize?,
|
||||
@ -173,7 +177,7 @@ public class Attachment {
|
||||
localRelativeFilePath: String,
|
||||
) {
|
||||
self.plaintextHash = plaintextHash
|
||||
self.mediaName = mediaName
|
||||
self.mediaName = Attachment.mediaName(plaintextHash: plaintextHash, encryptionKey: encryptionKey)
|
||||
self.encryptedByteCount = encryptedByteCount
|
||||
self.unencryptedByteCount = unencryptedByteCount
|
||||
self.cachedMediaSizePixels = cachedMediaSizePixels
|
||||
@ -279,13 +283,11 @@ public class Attachment {
|
||||
self.encryptionKey = record.encryptionKey
|
||||
self.originalAttachmentIdForQuotedReply = record.originalAttachmentIdForQuotedReply
|
||||
self.plaintextHash = record.plaintextHash
|
||||
self.mediaName = record.mediaName
|
||||
self.localRelativeFilePathThumbnail = record.localRelativeFilePathThumbnail
|
||||
self.lastFullscreenViewTimestamp = record.lastFullscreenViewTimestamp
|
||||
|
||||
if
|
||||
let plaintextHash = record.plaintextHash,
|
||||
let mediaName = record.mediaName,
|
||||
let encryptedByteCount = record.encryptedByteCount,
|
||||
let unencryptedByteCount = record.unencryptedByteCount,
|
||||
let ciphertextDigest = record.ciphertextDigest,
|
||||
@ -293,7 +295,7 @@ public class Attachment {
|
||||
{
|
||||
self.streamInfo = StreamInfo(
|
||||
plaintextHash: plaintextHash,
|
||||
mediaName: mediaName,
|
||||
encryptionKey: self.encryptionKey,
|
||||
encryptedByteCount: encryptedByteCount,
|
||||
unencryptedByteCount: unencryptedByteCount,
|
||||
cachedMediaSizePixels: {
|
||||
|
||||
@ -463,8 +463,7 @@ public class AttachmentManagerImpl: AttachmentManager {
|
||||
// duplicate to the first attachment), but this does drop cdn info if,
|
||||
// for example, this copy had valid cdn info and the older one did not.
|
||||
// It is on the exporter to dedupe and merge as needed so this doesn't happen.
|
||||
fallthrough
|
||||
case .duplicateMediaName(let existingAttachmentId):
|
||||
|
||||
// We already have an attachment with the same mediaName (likely from this same
|
||||
// backup). Just point the reference at the existing attachment.
|
||||
attachmentStore.addReference(
|
||||
@ -606,7 +605,6 @@ public class AttachmentManagerImpl: AttachmentManager {
|
||||
encryptionKey: pendingAttachment.encryptionKey,
|
||||
streamInfo: streamInfo,
|
||||
plaintextHash: pendingAttachment.plaintextHash,
|
||||
mediaName: pendingAttachment.mediaName,
|
||||
)
|
||||
|
||||
let hasOrphanRecord = orphanedAttachmentStore.orphanAttachmentExists(
|
||||
@ -729,11 +727,7 @@ public class AttachmentManagerImpl: AttachmentManager {
|
||||
switch error {
|
||||
case .duplicatePlaintextHash(let id):
|
||||
existingAttachmentId = id
|
||||
case .duplicateMediaName(let id):
|
||||
owsFailDebug("How did we match mediaName when using a random encryption key?")
|
||||
existingAttachmentId = id
|
||||
}
|
||||
|
||||
// Already have an attachment with the same plaintext hash or media name!
|
||||
// Move all existing references to that copy, instead.
|
||||
// Doing so should delete the original attachment pointer.
|
||||
@ -799,9 +793,7 @@ public class AttachmentManagerImpl: AttachmentManager {
|
||||
) throws -> Attachment.IDType {
|
||||
let existingAttachmentId: Attachment.IDType
|
||||
switch error {
|
||||
case
|
||||
.duplicatePlaintextHash(let id),
|
||||
.duplicateMediaName(let id):
|
||||
case .duplicatePlaintextHash(let id):
|
||||
existingAttachmentId = id
|
||||
}
|
||||
|
||||
|
||||
@ -10,10 +10,6 @@ public enum AttachmentInsertError: Error {
|
||||
/// attachment a duplicate. Callers should instead create a new owner reference to
|
||||
/// the same existing attachment.
|
||||
case duplicatePlaintextHash(existingAttachmentId: Attachment.IDType)
|
||||
/// An existing attachment was found with the same media name, making the new
|
||||
/// attachment a duplicate. Callers should instead create a new owner reference to
|
||||
/// the same existing attachment and possibly update it with any stream info.
|
||||
case duplicateMediaName(existingAttachmentId: Attachment.IDType)
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
@ -192,20 +188,6 @@ public struct AttachmentStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch an existing Attachment record with the given mediaName. There will
|
||||
/// be at most one.
|
||||
public func fetchAttachmentRecord(
|
||||
mediaName: String,
|
||||
tx: DBReadTransaction,
|
||||
) -> Attachment.Record? {
|
||||
let query = Attachment.Record
|
||||
.filter(Column(Attachment.Record.CodingKeys.mediaName) == mediaName)
|
||||
|
||||
return failIfThrows {
|
||||
try query.fetchOne(tx.database)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Fetch an arbitrary referenced attachment for the provided owner.
|
||||
@ -629,20 +611,6 @@ public struct AttachmentStore {
|
||||
throw AttachmentInsertError.duplicatePlaintextHash(existingAttachmentId: existingAttachmentId)
|
||||
}
|
||||
|
||||
// Find if there is already an attachment with the same media name.
|
||||
if
|
||||
let existingAttachmentId = fetchAttachmentRecord(
|
||||
mediaName: Attachment.mediaName(
|
||||
plaintextHash: streamInfo.plaintextHash,
|
||||
encryptionKey: attachment.encryptionKey,
|
||||
),
|
||||
tx: tx,
|
||||
)?.sqliteId,
|
||||
existingAttachmentId != attachment.id
|
||||
{
|
||||
throw AttachmentInsertError.duplicateMediaName(existingAttachmentId: existingAttachmentId)
|
||||
}
|
||||
|
||||
// We count it as a "view" if the download was initiated by the user
|
||||
let lastFullscreenViewTimestamp: UInt64?
|
||||
switch priority {
|
||||
@ -682,13 +650,11 @@ public struct AttachmentStore {
|
||||
attachment.streamInfo = streamInfo
|
||||
attachment.plaintextHash = streamInfo.plaintextHash
|
||||
attachment.latestTransitTierInfo = latestTransitTierInfo
|
||||
attachment.mediaName = streamInfo.mediaName
|
||||
attachment.lastFullscreenViewTimestamp = lastFullscreenViewTimestamp ?? attachment.lastFullscreenViewTimestamp
|
||||
case .mediaTierFullsize:
|
||||
attachment.streamInfo = streamInfo
|
||||
attachment.plaintextHash = streamInfo.plaintextHash
|
||||
attachment.latestTransitTierInfo = latestTransitTierInfo
|
||||
attachment.mediaName = streamInfo.mediaName
|
||||
if var mediaTierInfo = attachment.mediaTierInfo {
|
||||
// Wipe the last download attempt time; its now succeeded.
|
||||
mediaTierInfo.lastDownloadAttemptTimestamp = nil
|
||||
@ -735,7 +701,6 @@ public struct AttachmentStore {
|
||||
attachment.latestTransitTierInfo = latestTransitTierInfo
|
||||
attachment.originalTransitTierInfo = originalTransitTierInfo
|
||||
attachment.plaintextHash = streamInfo.plaintextHash
|
||||
attachment.mediaName = streamInfo.mediaName
|
||||
attachment.mediaTierInfo = mediaTierInfo
|
||||
attachment.thumbnailMediaTierInfo = thumbnailMediaTierInfo
|
||||
attachment.localRelativeFilePathThumbnail = nil
|
||||
@ -824,11 +789,9 @@ public struct AttachmentStore {
|
||||
public func saveMediaTierInfo(
|
||||
attachment: Attachment,
|
||||
mediaTierInfo: Attachment.MediaTierInfo,
|
||||
mediaName: String,
|
||||
tx: DBWriteTransaction,
|
||||
) {
|
||||
attachment.mediaTierInfo = mediaTierInfo
|
||||
attachment.mediaName = mediaName
|
||||
|
||||
let record = Attachment.Record(attachment: attachment)
|
||||
failIfThrows {
|
||||
@ -839,10 +802,8 @@ public struct AttachmentStore {
|
||||
func saveMediaTierThumbnailInfo(
|
||||
attachment: Attachment,
|
||||
thumbnailMediaTierInfo: Attachment.ThumbnailMediaTierInfo,
|
||||
mediaName: String,
|
||||
tx: DBWriteTransaction,
|
||||
) {
|
||||
attachment.mediaName = mediaName
|
||||
attachment.thumbnailMediaTierInfo = thumbnailMediaTierInfo
|
||||
|
||||
let record = Attachment.Record(attachment: attachment)
|
||||
@ -1081,17 +1042,6 @@ public struct AttachmentStore {
|
||||
throw AttachmentInsertError.duplicatePlaintextHash(existingAttachmentId: existingAttachmentId)
|
||||
}
|
||||
|
||||
// Find if there is already an attachment with the same media name.
|
||||
if
|
||||
let mediaName = attachmentRecord.mediaName,
|
||||
let existingAttachmentId = fetchAttachmentRecord(
|
||||
mediaName: mediaName,
|
||||
tx: tx,
|
||||
)?.sqliteId
|
||||
{
|
||||
throw AttachmentInsertError.duplicateMediaName(existingAttachmentId: existingAttachmentId)
|
||||
}
|
||||
|
||||
let attachment = failIfThrows {
|
||||
// Note that there are UNIQUE constraints on this table (e.g.,
|
||||
// plaintext hash and mediaName). Importantly, those are checked
|
||||
|
||||
@ -144,30 +144,16 @@ class AttachmentStoreTests: XCTestCase {
|
||||
|
||||
// MARK: -
|
||||
|
||||
func testInsertSameMediaName() {
|
||||
let mediaName = Data(repeating: 27, count: 10).hexadecimalString
|
||||
|
||||
switch testAttachmentInsertError(
|
||||
attachmentParams1: Attachment.Record.mockStream(streamInfo: .mock(mediaName: mediaName)),
|
||||
attachmentParams2: Attachment.Record.mockStream(streamInfo: .mock(mediaName: mediaName)),
|
||||
) {
|
||||
case .duplicateMediaName:
|
||||
break
|
||||
case nil, .duplicatePlaintextHash:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
func testInsertSamePlaintextHash() throws {
|
||||
let plaintextHash = UUID().data
|
||||
let plaintextHash = Randomness.generateRandomBytes(32)
|
||||
|
||||
switch testAttachmentInsertError(
|
||||
attachmentParams1: Attachment.Record.mockStream(streamInfo: .mock(plaintextHash: plaintextHash)),
|
||||
attachmentParams2: Attachment.Record.mockStream(streamInfo: .mock(plaintextHash: plaintextHash)),
|
||||
attachmentParams1: Attachment.Record.mockStream(plaintextHash: plaintextHash),
|
||||
attachmentParams2: Attachment.Record.mockStream(plaintextHash: plaintextHash),
|
||||
) {
|
||||
case .duplicatePlaintextHash:
|
||||
break
|
||||
case nil, .duplicateMediaName:
|
||||
case nil:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,10 +47,7 @@ public struct PendingAttachment {
|
||||
}
|
||||
|
||||
var mediaName: String {
|
||||
Attachment.mediaName(
|
||||
plaintextHash: plaintextHash,
|
||||
encryptionKey: encryptionKey,
|
||||
)
|
||||
return Attachment.mediaName(plaintextHash: self.plaintextHash, encryptionKey: self.encryptionKey)
|
||||
}
|
||||
|
||||
mutating func removeBorderlessRenderingFlagIfPresent() {
|
||||
|
||||
@ -2415,7 +2415,6 @@ public class AttachmentDownloadManagerImpl: AttachmentDownloadManager {
|
||||
encryptionKey: pendingAttachment.encryptionKey,
|
||||
streamInfo: streamInfo,
|
||||
plaintextHash: pendingAttachment.plaintextHash,
|
||||
mediaName: pendingAttachment.mediaName,
|
||||
)
|
||||
|
||||
let attachment = try self.attachmentStore.insert(
|
||||
@ -2589,7 +2588,6 @@ public class AttachmentDownloadManagerImpl: AttachmentDownloadManager {
|
||||
encryptionKey: pendingThumbnailAttachment.encryptionKey,
|
||||
streamInfo: streamInfo,
|
||||
plaintextHash: pendingThumbnailAttachment.plaintextHash,
|
||||
mediaName: pendingThumbnailAttachment.mediaName,
|
||||
)
|
||||
|
||||
let newAttachment = try self.attachmentStore.insert(
|
||||
|
||||
@ -10,9 +10,9 @@ import Foundation
|
||||
// MARK: - Infos
|
||||
|
||||
extension Attachment.StreamInfo {
|
||||
public static func mock(
|
||||
static func mock(
|
||||
plaintextHash: Data = Randomness.generateRandomBytes(32),
|
||||
mediaName: String = UUID().uuidString,
|
||||
encryptionKey: AttachmentKey = .generate(),
|
||||
encryptedByteCount: UInt32 = .random(in: 0..<95_000_000),
|
||||
unencryptedByteCount: UInt32 = .random(in: 0..<95_000_000),
|
||||
ciphertextDigest: Data = Randomness.generateRandomBytes(32),
|
||||
@ -20,7 +20,7 @@ extension Attachment.StreamInfo {
|
||||
) -> Attachment.StreamInfo {
|
||||
return Attachment.StreamInfo(
|
||||
plaintextHash: plaintextHash,
|
||||
mediaName: mediaName,
|
||||
encryptionKey: encryptionKey.combinedKey,
|
||||
encryptedByteCount: encryptedByteCount,
|
||||
unencryptedByteCount: unencryptedByteCount,
|
||||
cachedMediaSizePixels: nil,
|
||||
@ -109,22 +109,23 @@ extension Attachment.Record {
|
||||
)
|
||||
}
|
||||
|
||||
public static func mockStream(
|
||||
static func mockStream(
|
||||
blurHash: String? = UUID().uuidString,
|
||||
mimeType: String = MimeType.imageJpeg.rawValue,
|
||||
encryptionKey: Data = Randomness.generateRandomBytes(64),
|
||||
plaintextHash: Data? = nil,
|
||||
mediaName: String? = nil,
|
||||
streamInfo: Attachment.StreamInfo = .mock(),
|
||||
encryptionKey: AttachmentKey = .generate(),
|
||||
plaintextHash: Data = Randomness.generateRandomBytes(32),
|
||||
streamInfo: Attachment.StreamInfo? = nil,
|
||||
) -> Attachment.Record {
|
||||
return .forInsertingStream(
|
||||
blurHash: blurHash,
|
||||
mimeType: mimeType,
|
||||
contentType: Attachment.ContentType(mimeType: mimeType),
|
||||
encryptionKey: encryptionKey.combinedKey,
|
||||
streamInfo: streamInfo ?? .mock(
|
||||
plaintextHash: plaintextHash,
|
||||
encryptionKey: encryptionKey,
|
||||
streamInfo: streamInfo,
|
||||
plaintextHash: plaintextHash ?? streamInfo.plaintextHash,
|
||||
mediaName: mediaName ?? streamInfo.mediaName,
|
||||
),
|
||||
plaintextHash: plaintextHash,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -136,7 +137,6 @@ extension Attachment {
|
||||
mimeType: String = MimeType.applicationOctetStream.rawValue,
|
||||
encryptionKey: Data = Randomness.generateRandomBytes(64),
|
||||
plaintextHash: Data? = nil,
|
||||
mediaName: String? = nil,
|
||||
streamInfo: Attachment.StreamInfo? = nil,
|
||||
transitTierInfo: Attachment.TransitTierInfo? = nil,
|
||||
mediaTierInfo: Attachment.MediaTierInfo? = nil,
|
||||
@ -152,7 +152,6 @@ extension Attachment {
|
||||
contentType: Attachment.ContentType(mimeType: mimeType),
|
||||
encryptionKey: encryptionKey,
|
||||
plaintextHash: plaintextHash ?? streamInfo?.plaintextHash,
|
||||
mediaName: mediaName ?? streamInfo?.mediaName,
|
||||
localRelativeFilePathThumbnail: localRelativeFilePathThumbnail,
|
||||
streamInfo: streamInfo,
|
||||
latestTransitTierInfo: transitTierInfo,
|
||||
@ -169,10 +168,9 @@ extension Attachment {
|
||||
|
||||
extension AttachmentStream {
|
||||
|
||||
public static func mock(
|
||||
static func mock(
|
||||
blurHash: String? = nil,
|
||||
mimeType: String = MimeType.applicationOctetStream.rawValue,
|
||||
mediaName: String? = nil,
|
||||
streamInfo: Attachment.StreamInfo = .mock(),
|
||||
transitTierInfo: Attachment.TransitTierInfo? = nil,
|
||||
mediaTierInfo: Attachment.MediaTierInfo? = nil,
|
||||
@ -182,7 +180,6 @@ extension AttachmentStream {
|
||||
let attachment = Attachment.mock(
|
||||
blurHash: blurHash,
|
||||
mimeType: mimeType,
|
||||
mediaName: mediaName,
|
||||
streamInfo: streamInfo,
|
||||
transitTierInfo: transitTierInfo,
|
||||
mediaTierInfo: mediaTierInfo,
|
||||
|
||||
@ -32,7 +32,7 @@ extension Attachment {
|
||||
let latestTransitCiphertextDigest: Data?
|
||||
@DBUInt64Optional
|
||||
var latestTransitLastDownloadAttemptTimestamp: UInt64?
|
||||
let mediaName: String?
|
||||
private let _mediaName: String? // deprecated, always NULL
|
||||
let mediaTierCdnNumber: UInt32?
|
||||
let mediaTierUnencryptedByteCount: UInt32?
|
||||
let mediaTierUploadEra: String?
|
||||
@ -66,6 +66,12 @@ extension Attachment {
|
||||
let originalTransitTierIncrementalMac: Data?
|
||||
let originalTransitTierIncrementalMacChunkSize: UInt32?
|
||||
|
||||
var mediaName: String? {
|
||||
return self.plaintextHash.map {
|
||||
return Attachment.mediaName(plaintextHash: $0, encryptionKey: self.encryptionKey)
|
||||
}
|
||||
}
|
||||
|
||||
public var allFilesRelativePaths: [String] {
|
||||
return [
|
||||
localRelativeFilePath,
|
||||
@ -94,7 +100,7 @@ extension Attachment {
|
||||
case latestTransitUnencryptedByteCount = "transitUnencryptedByteCount"
|
||||
case latestTransitCiphertextDigest = "transitDigestSHA256Ciphertext"
|
||||
case latestTransitLastDownloadAttemptTimestamp = "lastTransitDownloadAttemptTimestamp"
|
||||
case mediaName
|
||||
case _mediaName = "mediaName"
|
||||
case mediaTierCdnNumber
|
||||
case mediaTierUnencryptedByteCount
|
||||
case mediaTierUploadEra
|
||||
@ -143,7 +149,6 @@ extension Attachment {
|
||||
contentType: attachment.contentType,
|
||||
encryptionKey: attachment.encryptionKey,
|
||||
plaintextHash: attachment.plaintextHash,
|
||||
mediaName: attachment.mediaName,
|
||||
localRelativeFilePathThumbnail: attachment.localRelativeFilePathThumbnail,
|
||||
streamInfo: attachment.streamInfo,
|
||||
latestTransitTierInfo: attachment.latestTransitTierInfo,
|
||||
@ -162,7 +167,6 @@ extension Attachment {
|
||||
contentType: Attachment.ContentType,
|
||||
encryptionKey: Data,
|
||||
plaintextHash: Data?,
|
||||
mediaName: String?,
|
||||
localRelativeFilePathThumbnail: String?,
|
||||
streamInfo: Attachment.StreamInfo?,
|
||||
latestTransitTierInfo: Attachment.TransitTierInfo?,
|
||||
@ -209,7 +213,7 @@ extension Attachment {
|
||||
self.originalTransitTierIncrementalMac = originalTransitTierInfo?.incrementalMacInfo?.mac
|
||||
self.originalTransitTierIncrementalMacChunkSize = originalTransitTierInfo?.incrementalMacInfo?.chunkSize
|
||||
|
||||
self.mediaName = mediaName
|
||||
self._mediaName = nil
|
||||
self.mediaTierCdnNumber = mediaTierInfo?.cdnNumber
|
||||
self.mediaTierUnencryptedByteCount = mediaTierInfo?.unencryptedByteCount
|
||||
self.mediaTierIncrementalMac = mediaTierInfo?.incrementalMacInfo?.mac
|
||||
@ -248,7 +252,6 @@ extension Attachment {
|
||||
contentType: contentType,
|
||||
encryptionKey: encryptionKey,
|
||||
plaintextHash: nil,
|
||||
mediaName: nil,
|
||||
localRelativeFilePathThumbnail: nil,
|
||||
streamInfo: nil,
|
||||
latestTransitTierInfo: latestTransitTierInfo,
|
||||
@ -268,7 +271,6 @@ extension Attachment {
|
||||
encryptionKey: Data,
|
||||
streamInfo: Attachment.StreamInfo,
|
||||
plaintextHash: Data,
|
||||
mediaName: String,
|
||||
) -> Record {
|
||||
return Record(
|
||||
sqliteId: nil,
|
||||
@ -277,7 +279,6 @@ extension Attachment {
|
||||
contentType: contentType,
|
||||
encryptionKey: encryptionKey,
|
||||
plaintextHash: plaintextHash,
|
||||
mediaName: mediaName,
|
||||
localRelativeFilePathThumbnail: nil,
|
||||
streamInfo: streamInfo,
|
||||
latestTransitTierInfo: nil,
|
||||
@ -306,9 +307,6 @@ extension Attachment {
|
||||
contentType: contentType,
|
||||
encryptionKey: encryptionKey,
|
||||
plaintextHash: plaintextHash,
|
||||
mediaName: plaintextHash.map {
|
||||
return Attachment.mediaName(plaintextHash: $0, encryptionKey: encryptionKey)
|
||||
},
|
||||
localRelativeFilePathThumbnail: nil,
|
||||
streamInfo: nil,
|
||||
latestTransitTierInfo: latestTransitTierInfo,
|
||||
@ -335,7 +333,6 @@ extension Attachment {
|
||||
// encryption key we use is irrelevant. Just generate a new one.
|
||||
encryptionKey: AttachmentKey.generate().combinedKey,
|
||||
plaintextHash: nil,
|
||||
mediaName: nil,
|
||||
localRelativeFilePathThumbnail: nil,
|
||||
streamInfo: nil,
|
||||
latestTransitTierInfo: nil,
|
||||
@ -362,7 +359,6 @@ extension Attachment {
|
||||
contentType: thumbnailContentType,
|
||||
encryptionKey: thumbnailEncryptionKey,
|
||||
plaintextHash: nil,
|
||||
mediaName: nil,
|
||||
localRelativeFilePathThumbnail: nil,
|
||||
streamInfo: nil,
|
||||
latestTransitTierInfo: thumbnailTransitTierInfo,
|
||||
|
||||
@ -334,6 +334,7 @@ public class GRDBSchemaMigrator {
|
||||
case purgeMyStoryDeletedAtTimestamp
|
||||
case addRecoverablePlaceholderExpirationIndex
|
||||
case backfillRecoverablePlaceholderErrorType
|
||||
case clearAttachmentMediaName
|
||||
|
||||
// NOTE: Every time we add a migration id, consider
|
||||
// incrementing grdbSchemaVersionLatest.
|
||||
@ -457,7 +458,7 @@ public class GRDBSchemaMigrator {
|
||||
}
|
||||
|
||||
public static let grdbSchemaVersionDefault: UInt = 0
|
||||
public static let grdbSchemaVersionLatest: UInt = 144
|
||||
public static let grdbSchemaVersionLatest: UInt = 145
|
||||
|
||||
private class DatabaseMigratorWrapper {
|
||||
// Run with immediate (or disabled) foreign key checks so that pre-existing
|
||||
@ -5220,6 +5221,29 @@ public class GRDBSchemaMigrator {
|
||||
return .success(())
|
||||
}
|
||||
|
||||
migrator.registerMigration(.clearAttachmentMediaName) { tx in
|
||||
try tx.database.execute(sql: "DROP TRIGGER IF EXISTS __Attachment_ad_backup_fullsize")
|
||||
try tx.database.execute(sql: "DROP TRIGGER IF EXISTS __Attachment_ad_backup_thumbnail")
|
||||
try tx.database.execute(sql: "UPDATE Attachment SET mediaName = NULL")
|
||||
try tx.database.execute(sql: """
|
||||
CREATE TRIGGER "__Attachment_ad_backup_fullsize" AFTER DELETE ON "Attachment"
|
||||
WHEN (OLD.mediaTierCdnNumber IS NOT NULL AND OLD.sha256ContentHash IS NOT NULL)
|
||||
BEGIN
|
||||
INSERT INTO OrphanedBackupAttachment (cdnNumber, mediaName, mediaId, type)
|
||||
VALUES (OLD.mediaTierCdnNumber, LOWER(HEX(OLD.sha256ContentHash || OLD.encryptionKey)), NULL, 0);
|
||||
END
|
||||
""")
|
||||
try tx.database.execute(sql: """
|
||||
CREATE TRIGGER "__Attachment_ad_backup_thumbnail" AFTER DELETE ON "Attachment"
|
||||
WHEN (OLD.thumbnailCdnNumber IS NOT NULL AND OLD.sha256ContentHash IS NOT NULL)
|
||||
BEGIN
|
||||
INSERT INTO OrphanedBackupAttachment (cdnNumber, mediaName, mediaId, type)
|
||||
VALUES (OLD.thumbnailCdnNumber, LOWER(HEX(OLD.sha256ContentHash || OLD.encryptionKey)), NULL, 1);
|
||||
END;
|
||||
""")
|
||||
return .success(())
|
||||
}
|
||||
|
||||
// MARK: - Schema Migration Insertion Point
|
||||
}
|
||||
|
||||
|
||||
@ -439,7 +439,6 @@ public actor AttachmentUploadManagerImpl: AttachmentUploadManager {
|
||||
attachmentStore.saveMediaTierInfo(
|
||||
attachment: attachmentStream.attachment,
|
||||
mediaTierInfo: mediaTierInfo,
|
||||
mediaName: attachmentStream.mediaName,
|
||||
tx: tx,
|
||||
)
|
||||
|
||||
@ -553,7 +552,6 @@ public actor AttachmentUploadManagerImpl: AttachmentUploadManager {
|
||||
attachmentStore.saveMediaTierThumbnailInfo(
|
||||
attachment: attachmentStream.attachment,
|
||||
thumbnailMediaTierInfo: thumbnailInfo,
|
||||
mediaName: attachmentStream.mediaName,
|
||||
tx: tx,
|
||||
)
|
||||
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import SignalServiceKit
|
||||
|
||||
struct OrphanedBackupAttachmentTest {
|
||||
@Test
|
||||
func testTriggers() throws {
|
||||
let db = InMemoryDB()
|
||||
|
||||
let encryptionKey = AttachmentKey.generate()
|
||||
let plaintextHash = Randomness.generateRandomBytes(32)
|
||||
var record = Attachment.Record.forInsertingFromBackup(
|
||||
blurHash: nil,
|
||||
mimeType: "image/png",
|
||||
contentType: .image,
|
||||
encryptionKey: encryptionKey.combinedKey,
|
||||
latestTransitTierInfo: nil,
|
||||
plaintextHash: plaintextHash,
|
||||
mediaTierInfo: Attachment.MediaTierInfo(
|
||||
cdnNumber: 2,
|
||||
unencryptedByteCount: 123,
|
||||
plaintextHash: plaintextHash,
|
||||
incrementalMacInfo: nil,
|
||||
uploadEra: "initialUploadEra",
|
||||
),
|
||||
thumbnailMediaTierInfo: Attachment.ThumbnailMediaTierInfo(
|
||||
cdnNumber: 5,
|
||||
uploadEra: "initialUploadEra",
|
||||
),
|
||||
)
|
||||
|
||||
try db.write { tx in
|
||||
try record.insert(tx.database)
|
||||
try record.delete(tx.database)
|
||||
}
|
||||
|
||||
let orphanedAttachments = try db.read { tx in
|
||||
return try OrphanedBackupAttachment.fetchAll(tx.database)
|
||||
}
|
||||
|
||||
struct OrphanedBackupAttachment2: Hashable {
|
||||
var mediaName: String?
|
||||
var cdnNumber: UInt32
|
||||
var type: Int?
|
||||
}
|
||||
|
||||
let actualValues = orphanedAttachments.map {
|
||||
return OrphanedBackupAttachment2(mediaName: $0.mediaName, cdnNumber: $0.cdnNumber, type: $0.type?.rawValue)
|
||||
}
|
||||
let mediaName = (plaintextHash + encryptionKey.combinedKey).hexadecimalString
|
||||
let expectedValues = [
|
||||
OrphanedBackupAttachment2(mediaName: mediaName, cdnNumber: 2, type: OrphanedBackupAttachment.SizeType.fullsize.rawValue),
|
||||
OrphanedBackupAttachment2(mediaName: mediaName, cdnNumber: 5, type: OrphanedBackupAttachment.SizeType.thumbnail.rawValue),
|
||||
]
|
||||
#expect(Set(actualValues) == Set(expectedValues))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user