diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 30fc36ca15..ad34083375 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -984,7 +984,6 @@ 665C0D5E2ADF53E200539A37 /* BackupArchiveManagerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665C0D5D2ADF53E200539A37 /* BackupArchiveManagerImpl.swift */; }; 665C0D602ADF57D000539A37 /* BackupArchive+Shims.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665C0D5F2ADF57D000539A37 /* BackupArchive+Shims.swift */; }; 665C0D622AE0552900539A37 /* BackupArchiveProtoStreamProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665C0D612AE0552900539A37 /* BackupArchiveProtoStreamProvider.swift */; }; - 665C758C2C35A55300D2E4BA /* TSAttachmentMigration+ThreadWallpaper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665C758B2C35A55300D2E4BA /* TSAttachmentMigration+ThreadWallpaper.swift */; }; 665CBD052BADC87A0059EA4F /* DraftQuotedReplyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665CBD042BADC87A0059EA4F /* DraftQuotedReplyModel.swift */; }; 665D9B452C111C6D00E73E94 /* AttachmentMultisend+OversizeText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665D9B442C111C6D00E73E94 /* AttachmentMultisend+OversizeText.swift */; }; 665EF86D290C385B00F490D2 /* OWSNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665EF86C290C385B00F490D2 /* OWSNavigationController.swift */; }; @@ -992,7 +991,6 @@ 665FAE8C2A02C0D400FA298D /* SpoilerRevealState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665FAE8B2A02C0D400FA298D /* SpoilerRevealState.swift */; }; 6660725E2BAB36960084B3D2 /* AttachmentDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6660725D2BAB36960084B3D2 /* AttachmentDataSource.swift */; }; 666072622BAB58A20084B3D2 /* OWSContactSerializationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 666072602BAB58850084B3D2 /* OWSContactSerializationTest.swift */; }; - 6660C7972C45C34A00D9C30A /* TSAttachmentMigration+TSMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6660C7962C45C34A00D9C30A /* TSAttachmentMigration+TSMessage.swift */; }; 6664B9AB2A314EBD008EF74B /* SpoilerRevealStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6664B9AA2A314EBD008EF74B /* SpoilerRevealStateTests.swift */; }; 666654212AD0B03F00B23B32 /* MasterKeySyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 666654202AD0B03F00B23B32 /* MasterKeySyncManager.swift */; }; 66681CDF2C58174F00E50136 /* BackupAttachmentDownloadStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66681CDE2C58174F00E50136 /* BackupAttachmentDownloadStoreTests.swift */; }; @@ -1085,13 +1083,6 @@ 6691E7EF2996E8FB0032A68A /* TSRequestOWSURLSessionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6691E7EE2996E8FB0032A68A /* TSRequestOWSURLSessionMock.swift */; }; 6691E7F22996E9BC0032A68A /* RegistrationSessionManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6691E7F12996E9BC0032A68A /* RegistrationSessionManagerMock.swift */; }; 6691E7F72996EAD70032A68A /* SecureValueRecoveryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6691E7F62996EAD70032A68A /* SecureValueRecoveryMock.swift */; }; - 669379ED2C3C5B2C00EED7A0 /* TSAttachmentMigration+Records.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669379EC2C3C5B2C00EED7A0 /* TSAttachmentMigration+Records.swift */; }; - 669379EF2C3C5E5800EED7A0 /* TSAttachmentMigration+AudioWaveformManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669379EE2C3C5E5800EED7A0 /* TSAttachmentMigration+AudioWaveformManager.swift */; }; - 669379F12C3C79E800EED7A0 /* TSAttachmentMigration+OWSMediaUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669379F02C3C79E800EED7A0 /* TSAttachmentMigration+OWSMediaUtils.swift */; }; - 669379F32C3C7C3B00EED7A0 /* TSAttachmentMigration+OWSImageSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669379F22C3C7C3B00EED7A0 /* TSAttachmentMigration+OWSImageSource.swift */; }; - 669379F52C3C7EA800EED7A0 /* TSAttachmentMigration+ImageMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669379F42C3C7EA800EED7A0 /* TSAttachmentMigration+ImageMetadata.swift */; }; - 669379F72C3C847000EED7A0 /* TSAttachmentMigration+AttachmentValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669379F62C3C847000EED7A0 /* TSAttachmentMigration+AttachmentValidator.swift */; }; - 66937A032C3F4EFC00EED7A0 /* TSAttachmentMigration+StoryMessageAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66937A022C3F4EFC00EED7A0 /* TSAttachmentMigration+StoryMessageAttachment.swift */; }; 6694BAB32CE5792B0015633F /* BackupArchiveProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6694BAB22CE579270015633F /* BackupArchiveProgress.swift */; }; 6694BF682B36484900B18764 /* PinnedThreadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6694BF672B36484800B18764 /* PinnedThreadManager.swift */; }; 6694BF6A2B3650E400B18764 /* PinnedThreadStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6694BF692B3650E400B18764 /* PinnedThreadStore.swift */; }; @@ -1118,7 +1109,6 @@ 669E900728B43F5B00043D28 /* SystemStoryManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669E900628B43F5B00043D28 /* SystemStoryManagerProtocol.swift */; }; 669E901028B57D6300043D28 /* SystemStoryManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669E900F28B57D6300043D28 /* SystemStoryManagerMock.swift */; }; 669FAE1B2B7AC919009EE2FE /* OWSLinkPreviewSerializationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669FAE1A2B7AC919009EE2FE /* OWSLinkPreviewSerializationTest.swift */; }; - 66A1ABE22C3311B40033C5EB /* TSAttachmentMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66A1ABE12C3311B40033C5EB /* TSAttachmentMigration.swift */; }; 66A1DF73298C635E00C4E4A7 /* RegistrationRequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66A1DF72298C635E00C4E4A7 /* RegistrationRequestFactory.swift */; }; 66A1DF75298C73D900C4E4A7 /* RegistrationServiceResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66A1DF74298C73D900C4E4A7 /* RegistrationServiceResponses.swift */; }; 66A1F4E22E035C020095DE4B /* BackupExportJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66A1F4E12E035BFE0095DE4B /* BackupExportJob.swift */; }; @@ -1143,7 +1133,6 @@ 66B152AC2DD6FDA700DE25CC /* AttachmentOffloadingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66B152AB2DD6FD9400DE25CC /* AttachmentOffloadingManager.swift */; }; 66B1E26C2CB187B3005F43AC /* AttachmentUploadStoreImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66B1E26B2CB187A0005F43AC /* AttachmentUploadStoreImpl.swift */; }; 66B1E2702CB48C53005F43AC /* Array+SSKTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66B1E26F2CB48C48005F43AC /* Array+SSKTest.swift */; }; - 66B2FBFE2D10F5EB00189908 /* IncrementalMessageTSAttachmentMigratorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66B2FBFD2D10F5DE00189908 /* IncrementalMessageTSAttachmentMigratorFactory.swift */; }; 66B5451A2DD5B9A00016289B /* BackupListMediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66B545192DD5B9980016289B /* BackupListMediaManager.swift */; }; 66B78E032BE59B860022580E /* StickerMetadata+TSResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66B78E022BE59B860022580E /* StickerMetadata+TSResource.swift */; }; 66B78E062BE5AADF0022580E /* AttachmentViewOnceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66B78E052BE5AADF0022580E /* AttachmentViewOnceManager.swift */; }; @@ -1164,9 +1153,6 @@ 66C1A8802BB77EA50076C65A /* AttachmentUploadManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C1A87E2BB77E950076C65A /* AttachmentUploadManagerTests.swift */; }; 66C1A8852BB77EE00076C65A /* AttachmentUploadManagerTestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C1A8812BB77EBB0076C65A /* AttachmentUploadManagerTestHelper.swift */; }; 66C1A8862BB77EE30076C65A /* AttachmentUploadManagerTestMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C1A8832BB77EC60076C65A /* AttachmentUploadManagerTestMocks.swift */; }; - 66C1BF512D0CC7C9002296F7 /* IncrementalTSAttachmentMigrationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C1BF502D0CC7C7002296F7 /* IncrementalTSAttachmentMigrationStore.swift */; }; - 66C1BF532D0CC7EB002296F7 /* IncrementalMessageTSAttachmentMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C1BF522D0CC7DB002296F7 /* IncrementalMessageTSAttachmentMigrator.swift */; }; - 66C1BF552D0CC88A002296F7 /* IncrementalMessageTSAttachmentMigrationRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C1BF542D0CC881002296F7 /* IncrementalMessageTSAttachmentMigrationRunner.swift */; }; 66C2B1312A05D28A008DDE72 /* TSRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C2B1302A05D28A008DDE72 /* TSRequest.swift */; }; 66C2B1362A0DB02E008DDE72 /* SVRUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C2B1352A0DB02E008DDE72 /* SVRUtil.swift */; }; 66C2B1382A0DB6A9008DDE72 /* SVRAuthCredential.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C2B1372A0DB6A9008DDE72 /* SVRAuthCredential.swift */; }; @@ -1263,8 +1249,6 @@ 66F98DE62DBBED6C009F1A86 /* LineWrappingStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F98DE52DBBED68009F1A86 /* LineWrappingStackView.swift */; }; 66F98DE82DBBF25A009F1A86 /* LineWrappingStackViewTestController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F98DE72DBBF24F009F1A86 /* LineWrappingStackViewTestController.swift */; }; 66F98DEA2DC53155009F1A86 /* ChatListViewController+BackupDownloadProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F98DE92DC5314E009F1A86 /* ChatListViewController+BackupDownloadProgressView.swift */; }; - 66FA12B42E9971E300A1F3C2 /* AVAssetReaderTrackOutputWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 66FA12B32E9971E300A1F3C2 /* AVAssetReaderTrackOutputWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 66FA12B62E99727300A1F3C2 /* AVAssetReaderTrackOutputWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 66FA12B52E99726C00A1F3C2 /* AVAssetReaderTrackOutputWrapper.m */; settings = {COMPILER_FLAGS = "-fobjc-exceptions"; }; }; 66FA2B1D28CB0DE1006845CD /* PaymentsBiometryLockPromptViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FA2B1C28CB0DE1006845CD /* PaymentsBiometryLockPromptViewController.swift */; }; 66FBC4E128DA820900BD9E8B /* MyStorySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FBC4E028DA820900BD9E8B /* MyStorySettingsViewController.swift */; }; 66FBC4E328DA82AA00BD9E8B /* SelectMyStoryRecipientsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FBC4E228DA82AA00BD9E8B /* SelectMyStoryRecipientsViewController.swift */; }; @@ -5158,7 +5142,6 @@ 665C0D5D2ADF53E200539A37 /* BackupArchiveManagerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveManagerImpl.swift; sourceTree = ""; }; 665C0D5F2ADF57D000539A37 /* BackupArchive+Shims.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackupArchive+Shims.swift"; sourceTree = ""; }; 665C0D612AE0552900539A37 /* BackupArchiveProtoStreamProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveProtoStreamProvider.swift; sourceTree = ""; }; - 665C758B2C35A55300D2E4BA /* TSAttachmentMigration+ThreadWallpaper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSAttachmentMigration+ThreadWallpaper.swift"; sourceTree = ""; }; 665CBD042BADC87A0059EA4F /* DraftQuotedReplyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftQuotedReplyModel.swift; sourceTree = ""; }; 665D9B442C111C6D00E73E94 /* AttachmentMultisend+OversizeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentMultisend+OversizeText.swift"; sourceTree = ""; }; 665EF86C290C385B00F490D2 /* OWSNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSNavigationController.swift; sourceTree = ""; }; @@ -5166,7 +5149,6 @@ 665FAE8B2A02C0D400FA298D /* SpoilerRevealState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpoilerRevealState.swift; sourceTree = ""; }; 6660725D2BAB36960084B3D2 /* AttachmentDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDataSource.swift; sourceTree = ""; }; 666072602BAB58850084B3D2 /* OWSContactSerializationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSContactSerializationTest.swift; sourceTree = ""; }; - 6660C7962C45C34A00D9C30A /* TSAttachmentMigration+TSMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSAttachmentMigration+TSMessage.swift"; sourceTree = ""; }; 6664B9AA2A314EBD008EF74B /* SpoilerRevealStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpoilerRevealStateTests.swift; sourceTree = ""; }; 666654202AD0B03F00B23B32 /* MasterKeySyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterKeySyncManager.swift; sourceTree = ""; }; 66681CDE2C58174F00E50136 /* BackupAttachmentDownloadStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupAttachmentDownloadStoreTests.swift; sourceTree = ""; }; @@ -5261,13 +5243,6 @@ 6691E7EE2996E8FB0032A68A /* TSRequestOWSURLSessionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSRequestOWSURLSessionMock.swift; sourceTree = ""; }; 6691E7F12996E9BC0032A68A /* RegistrationSessionManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationSessionManagerMock.swift; sourceTree = ""; }; 6691E7F62996EAD70032A68A /* SecureValueRecoveryMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureValueRecoveryMock.swift; sourceTree = ""; }; - 669379EC2C3C5B2C00EED7A0 /* TSAttachmentMigration+Records.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSAttachmentMigration+Records.swift"; sourceTree = ""; }; - 669379EE2C3C5E5800EED7A0 /* TSAttachmentMigration+AudioWaveformManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSAttachmentMigration+AudioWaveformManager.swift"; sourceTree = ""; }; - 669379F02C3C79E800EED7A0 /* TSAttachmentMigration+OWSMediaUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSAttachmentMigration+OWSMediaUtils.swift"; sourceTree = ""; }; - 669379F22C3C7C3B00EED7A0 /* TSAttachmentMigration+OWSImageSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSAttachmentMigration+OWSImageSource.swift"; sourceTree = ""; }; - 669379F42C3C7EA800EED7A0 /* TSAttachmentMigration+ImageMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSAttachmentMigration+ImageMetadata.swift"; sourceTree = ""; }; - 669379F62C3C847000EED7A0 /* TSAttachmentMigration+AttachmentValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSAttachmentMigration+AttachmentValidator.swift"; sourceTree = ""; }; - 66937A022C3F4EFC00EED7A0 /* TSAttachmentMigration+StoryMessageAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSAttachmentMigration+StoryMessageAttachment.swift"; sourceTree = ""; }; 6694BAB22CE579270015633F /* BackupArchiveProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveProgress.swift; sourceTree = ""; }; 6694BF672B36484800B18764 /* PinnedThreadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedThreadManager.swift; sourceTree = ""; }; 6694BF692B3650E400B18764 /* PinnedThreadStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedThreadStore.swift; sourceTree = ""; }; @@ -5296,7 +5271,6 @@ 669E900628B43F5B00043D28 /* SystemStoryManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemStoryManagerProtocol.swift; sourceTree = ""; }; 669E900F28B57D6300043D28 /* SystemStoryManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemStoryManagerMock.swift; sourceTree = ""; }; 669FAE1A2B7AC919009EE2FE /* OWSLinkPreviewSerializationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSLinkPreviewSerializationTest.swift; sourceTree = ""; }; - 66A1ABE12C3311B40033C5EB /* TSAttachmentMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSAttachmentMigration.swift; sourceTree = ""; }; 66A1DF72298C635E00C4E4A7 /* RegistrationRequestFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationRequestFactory.swift; sourceTree = ""; }; 66A1DF74298C73D900C4E4A7 /* RegistrationServiceResponses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationServiceResponses.swift; sourceTree = ""; }; 66A1F4E12E035BFE0095DE4B /* BackupExportJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupExportJob.swift; sourceTree = ""; }; @@ -5320,7 +5294,6 @@ 66B152AB2DD6FD9400DE25CC /* AttachmentOffloadingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentOffloadingManager.swift; sourceTree = ""; }; 66B1E26B2CB187A0005F43AC /* AttachmentUploadStoreImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploadStoreImpl.swift; sourceTree = ""; }; 66B1E26F2CB48C48005F43AC /* Array+SSKTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+SSKTest.swift"; sourceTree = ""; }; - 66B2FBFD2D10F5DE00189908 /* IncrementalMessageTSAttachmentMigratorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncrementalMessageTSAttachmentMigratorFactory.swift; sourceTree = ""; }; 66B545192DD5B9980016289B /* BackupListMediaManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupListMediaManager.swift; sourceTree = ""; }; 66B78E022BE59B860022580E /* StickerMetadata+TSResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StickerMetadata+TSResource.swift"; sourceTree = ""; }; 66B78E052BE5AADF0022580E /* AttachmentViewOnceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentViewOnceManager.swift; sourceTree = ""; }; @@ -5341,9 +5314,6 @@ 66C1A87E2BB77E950076C65A /* AttachmentUploadManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploadManagerTests.swift; sourceTree = ""; }; 66C1A8812BB77EBB0076C65A /* AttachmentUploadManagerTestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploadManagerTestHelper.swift; sourceTree = ""; }; 66C1A8832BB77EC60076C65A /* AttachmentUploadManagerTestMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploadManagerTestMocks.swift; sourceTree = ""; }; - 66C1BF502D0CC7C7002296F7 /* IncrementalTSAttachmentMigrationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncrementalTSAttachmentMigrationStore.swift; sourceTree = ""; }; - 66C1BF522D0CC7DB002296F7 /* IncrementalMessageTSAttachmentMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncrementalMessageTSAttachmentMigrator.swift; sourceTree = ""; }; - 66C1BF542D0CC881002296F7 /* IncrementalMessageTSAttachmentMigrationRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncrementalMessageTSAttachmentMigrationRunner.swift; sourceTree = ""; }; 66C2B1302A05D28A008DDE72 /* TSRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSRequest.swift; sourceTree = ""; }; 66C2B1352A0DB02E008DDE72 /* SVRUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVRUtil.swift; sourceTree = ""; }; 66C2B1372A0DB6A9008DDE72 /* SVRAuthCredential.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVRAuthCredential.swift; sourceTree = ""; }; @@ -5441,8 +5411,6 @@ 66F98DE52DBBED68009F1A86 /* LineWrappingStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineWrappingStackView.swift; sourceTree = ""; }; 66F98DE72DBBF24F009F1A86 /* LineWrappingStackViewTestController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineWrappingStackViewTestController.swift; sourceTree = ""; }; 66F98DE92DC5314E009F1A86 /* ChatListViewController+BackupDownloadProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatListViewController+BackupDownloadProgressView.swift"; sourceTree = ""; }; - 66FA12B32E9971E300A1F3C2 /* AVAssetReaderTrackOutputWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AVAssetReaderTrackOutputWrapper.h; sourceTree = ""; }; - 66FA12B52E99726C00A1F3C2 /* AVAssetReaderTrackOutputWrapper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AVAssetReaderTrackOutputWrapper.m; sourceTree = ""; }; 66FA2B1C28CB0DE1006845CD /* PaymentsBiometryLockPromptViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsBiometryLockPromptViewController.swift; sourceTree = ""; }; 66FA2B1E28CBA4A5006845CD /* DeviceOwnerAuthenticationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceOwnerAuthenticationType.swift; sourceTree = ""; }; 66FBC4E028DA820900BD9E8B /* MyStorySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyStorySettingsViewController.swift; sourceTree = ""; }; @@ -10399,33 +10367,10 @@ 66A1ABDF2C3311800033C5EB /* IncrementalMigrations */ = { isa = PBXGroup; children = ( - 66A1ABE02C33118A0033C5EB /* TSAttachment */, ); path = IncrementalMigrations; sourceTree = ""; }; - 66A1ABE02C33118A0033C5EB /* TSAttachment */ = { - isa = PBXGroup; - children = ( - 66FA12B32E9971E300A1F3C2 /* AVAssetReaderTrackOutputWrapper.h */, - 66FA12B52E99726C00A1F3C2 /* AVAssetReaderTrackOutputWrapper.m */, - 66C1BF522D0CC7DB002296F7 /* IncrementalMessageTSAttachmentMigrator.swift */, - 66B2FBFD2D10F5DE00189908 /* IncrementalMessageTSAttachmentMigratorFactory.swift */, - 66C1BF502D0CC7C7002296F7 /* IncrementalTSAttachmentMigrationStore.swift */, - 669379F62C3C847000EED7A0 /* TSAttachmentMigration+AttachmentValidator.swift */, - 669379EE2C3C5E5800EED7A0 /* TSAttachmentMigration+AudioWaveformManager.swift */, - 669379F42C3C7EA800EED7A0 /* TSAttachmentMigration+ImageMetadata.swift */, - 669379F22C3C7C3B00EED7A0 /* TSAttachmentMigration+OWSImageSource.swift */, - 669379F02C3C79E800EED7A0 /* TSAttachmentMigration+OWSMediaUtils.swift */, - 669379EC2C3C5B2C00EED7A0 /* TSAttachmentMigration+Records.swift */, - 66937A022C3F4EFC00EED7A0 /* TSAttachmentMigration+StoryMessageAttachment.swift */, - 665C758B2C35A55300D2E4BA /* TSAttachmentMigration+ThreadWallpaper.swift */, - 6660C7962C45C34A00D9C30A /* TSAttachmentMigration+TSMessage.swift */, - 66A1ABE12C3311B40033C5EB /* TSAttachmentMigration.swift */, - ); - path = TSAttachment; - sourceTree = ""; - }; 66A1DF71298C634500C4E4A7 /* Registration */ = { isa = PBXGroup; children = ( @@ -10974,7 +10919,6 @@ 66DA8DF92C91125200799E70 /* AttachmentValidationBackfillRunner.swift */, 66A1F4E52E0364140095DE4B /* BackupBGProcessingTaskRunner.swift */, 66DA8DF72C910D3B00799E70 /* BGProcessingTaskRunner.swift */, - 66C1BF542D0CC881002296F7 /* IncrementalMessageTSAttachmentMigrationRunner.swift */, 66CDB7512AF9D117009A36EC /* MessageFetchBGRefreshTask.swift */, ); path = src; @@ -15330,7 +15274,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 66FA12B42E9971E300A1F3C2 /* AVAssetReaderTrackOutputWrapper.h in Headers */, F9C5CD77289453B300548EEE /* BaseModel.h in Headers */, 668A00E02C2B5ECF007B8808 /* DebuggerUtils.h in Headers */, F9C5CC0A289453B300548EEE /* InstalledSticker.h in Headers */, @@ -18041,7 +17984,6 @@ 04BBBE902E259A6900E914B1 /* InactivePrimaryDeviceReminderMegaphone.swift in Sources */, D9E43C052CC194140001536E /* IncomingCallControls.swift in Sources */, D9E43C062CC194140001536E /* IncomingReactionsView.swift in Sources */, - 66C1BF552D0CC88A002296F7 /* IncrementalMessageTSAttachmentMigrationRunner.swift in Sources */, D9E43C2A2CC194140001536E /* IndividualCall.swift in Sources */, D9E43C2B2CC194140001536E /* IndividualCallService.swift in Sources */, D9E43C072CC194140001536E /* IndividualCallViewController.swift in Sources */, @@ -18543,7 +18485,6 @@ 500AEE092A4E09AD00371F05 /* AuthorMergeObserver.swift in Sources */, 6649651C2BDC6EAD00E2DE98 /* AVAsset+Attachment.swift in Sources */, 058B49932C66805500307D38 /* AVAssetExportSession+Async.swift in Sources */, - 66FA12B62E99727300A1F3C2 /* AVAssetReaderTrackOutputWrapper.m in Sources */, 7254651E2BA012BD00EABFD2 /* AvatarBuilder.swift in Sources */, D93F4D5A2D800DD20042926C /* AvatarDefaultColorManager.swift in Sources */, 720547F22B9C8F9900E2CF2F /* AvatarModel.swift in Sources */, @@ -18921,9 +18862,6 @@ D9AE0ACF29186D7F0063488B /* IncomingContactSyncJobRecord.swift in Sources */, 6640DD602ACDBEC500CE9A8C /* IncomingPniChangeNumberProcessor.swift in Sources */, F9C5CC69289453B300548EEE /* IncompleteCallsJob.swift in Sources */, - 66C1BF532D0CC7EB002296F7 /* IncrementalMessageTSAttachmentMigrator.swift in Sources */, - 66B2FBFE2D10F5EB00189908 /* IncrementalMessageTSAttachmentMigratorFactory.swift in Sources */, - 66C1BF512D0CC7C9002296F7 /* IncrementalTSAttachmentMigrationStore.swift in Sources */, D979CC262AD3933B006AAC49 /* IndividualCallRecordManager.swift in Sources */, D9B95A9B29E8923B00D7CB95 /* InMemoryDB.swift in Sources */, C1DD78AB2BB1CEF80020F064 /* InputStreamable.swift in Sources */, @@ -19540,16 +19478,6 @@ 661170C42ABA4D9900A1B16D /* TSAccountManager.swift in Sources */, 661170C82ABA4F3A00A1B16D /* TSAccountManagerImpl.swift in Sources */, 664657472ACB66630099DE1C /* TSAccountManagerObjcBridge.swift in Sources */, - 669379F72C3C847000EED7A0 /* TSAttachmentMigration+AttachmentValidator.swift in Sources */, - 669379EF2C3C5E5800EED7A0 /* TSAttachmentMigration+AudioWaveformManager.swift in Sources */, - 669379F52C3C7EA800EED7A0 /* TSAttachmentMigration+ImageMetadata.swift in Sources */, - 669379F32C3C7C3B00EED7A0 /* TSAttachmentMigration+OWSImageSource.swift in Sources */, - 669379F12C3C79E800EED7A0 /* TSAttachmentMigration+OWSMediaUtils.swift in Sources */, - 669379ED2C3C5B2C00EED7A0 /* TSAttachmentMigration+Records.swift in Sources */, - 66937A032C3F4EFC00EED7A0 /* TSAttachmentMigration+StoryMessageAttachment.swift in Sources */, - 665C758C2C35A55300D2E4BA /* TSAttachmentMigration+ThreadWallpaper.swift in Sources */, - 6660C7972C45C34A00D9C30A /* TSAttachmentMigration+TSMessage.swift in Sources */, - 66A1ABE22C3311B40033C5EB /* TSAttachmentMigration.swift in Sources */, F9C5CC5C289453B300548EEE /* TSCall+SDS.swift in Sources */, F9C5CC5D289453B300548EEE /* TSCall.m in Sources */, D91AC93E2B6337B200814975 /* TSCall.swift in Sources */, diff --git a/Signal/AppLaunch/AppDelegate.swift b/Signal/AppLaunch/AppDelegate.swift index de454cf102..6e9d5d3263 100644 --- a/Signal/AppLaunch/AppDelegate.swift +++ b/Signal/AppLaunch/AppDelegate.swift @@ -17,7 +17,6 @@ enum LaunchPreflightError { case databaseCorruptedAndMightBeRecoverable case databaseUnrecoverablyCorrupted case lastAppLaunchCrashed - case incrementalTSAttachmentMigrationFailed case lowStorageSpaceAvailable case possibleReadCorruptionCrashed @@ -33,8 +32,6 @@ enum LaunchPreflightError { return "LaunchFailure_DatabaseUnrecoverablyCorrupted" case .lastAppLaunchCrashed: return "LaunchFailure_LastAppLaunchCrashed" - case .incrementalTSAttachmentMigrationFailed: - return "LaunchFailure_incrementalTSAttachmentMigrationFailed" case .lowStorageSpaceAvailable: return "LaunchFailure_NoDiskSpaceAvailable" case .possibleReadCorruptionCrashed: @@ -259,24 +256,12 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // Do this even if `appVersion` isn't used -- there's side effects. let appVersion = AppVersionImpl.shared - // Set up and register incremental migration for TSAttachment -> v2 Attachment. - // TODO: remove this (and the incremental migrator itself) once we make this - // migration a launch-blocking GRDB migration. - let incrementalMessageTSAttachmentMigrationStore = IncrementalTSAttachmentMigrationStore( - userDefaults: mainAppContext.appUserDefaults() - ) - let incrementalMessageTSAttachmentMigratorFactory = IncrementalMessageTSAttachmentMigratorFactoryImpl( - store: incrementalMessageTSAttachmentMigrationStore - ) - let launchContext = LaunchContext( appContext: mainAppContext, databaseStorage: databaseStorage, deviceSleepManager: deviceSleepManager, keychainStorage: keychainStorage, launchStartedAt: launchStartedAt, - incrementalMessageTSAttachmentMigrationStore: incrementalMessageTSAttachmentMigrationStore, - incrementalMessageTSAttachmentMigratorFactory: incrementalMessageTSAttachmentMigratorFactory ) // We need to do this _after_ we set up logging, when the keychain is unlocked, @@ -284,7 +269,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { let preflightError = checkIfAllowedToLaunch( mainAppContext: mainAppContext, appVersion: appVersion, - incrementalTSAttachmentMigrationStore: incrementalMessageTSAttachmentMigrationStore, didDeviceTransferRestoreSucceed: didDeviceTransferRestoreSucceed ) @@ -311,14 +295,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // We _must_ register BGProcessingTask handlers synchronously in didFinishLaunching. // https://developer.apple.com/documentation/backgroundtasks/bgtaskscheduler/register(fortaskwithidentifier:using:launchhandler:) // WARNING: Apple docs say we can only have 10 BGProcessingTasks registered. - let attachmentMigrationRunner = IncrementalMessageTSAttachmentMigrationRunner( - appContext: mainAppContext, - db: databaseStorage, - store: incrementalMessageTSAttachmentMigrationStore, - migrator: { DependenciesBridge.shared.incrementalMessageTSAttachmentMigrator } - ) - attachmentMigrationRunner.registerBGProcessingTask(appReadiness: appReadiness) - let attachmentBackfillStore = AttachmentValidationBackfillStore() let attachmentValidationRunner = AttachmentValidationBackfillRunner( db: databaseStorage, @@ -345,9 +321,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { databaseMigratorRunner.registerBGProcessingTask(appReadiness: appReadiness) appReadiness.runNowOrWhenAppDidBecomeReadyAsync { - if SSKEnvironment.shared.remoteConfigManagerRef.currentConfig().shouldRunTSAttachmentMigrationInBGProcessingTask { - attachmentMigrationRunner.scheduleBGProcessingTaskIfNeeded() - } attachmentValidationRunner.scheduleBGProcessingTaskIfNeeded() backupRunner.scheduleBGProcessingTaskIfNeeded() } @@ -399,8 +372,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { let deviceSleepManager: DeviceSleepManagerImpl let keychainStorage: any KeychainStorage let launchStartedAt: CFTimeInterval - let incrementalMessageTSAttachmentMigrationStore: IncrementalTSAttachmentMigrationStore - let incrementalMessageTSAttachmentMigratorFactory: IncrementalMessageTSAttachmentMigratorFactory } private func launchApp( @@ -451,7 +422,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { callMessageHandler: WebRTCCallMessageHandler(), currentCallProvider: currentCall, notificationPresenter: NotificationPresenterImpl(), - incrementalMessageTSAttachmentMigratorFactory: launchContext.incrementalMessageTSAttachmentMigratorFactory ) SUIEnvironment.shared.setUp( appReadiness: appReadiness, @@ -478,20 +448,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { ) let finalContinuation = await dataMigrationContinuation.migrateDatabaseData() finalContinuation.runLaunchTasksIfNeededAndReloadCaches() - guard BuildFlags.runTSAttachmentMigrationBlockingOnLaunch else { - return (finalContinuation, sleepBlockObject) - } - let progressSink = OWSProgress.createSink { [weak loadingViewController] progress in - await MainActor.run { - loadingViewController?.updateProgress(progress) - } - } - let migrateTask = Task { - _ = await finalContinuation.dependenciesBridge.incrementalMessageTSAttachmentMigrator - .runInMainAppUntilFinished(ignorePastFailures: true, progress: progressSink) - } - await migrateTask.value return (finalContinuation, sleepBlockObject) } @@ -1014,7 +971,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { private func checkIfAllowedToLaunch( mainAppContext: MainAppContext, appVersion: AppVersion, - incrementalTSAttachmentMigrationStore: IncrementalTSAttachmentMigrationStore, didDeviceTransferRestoreSucceed: Bool ) -> LaunchPreflightError? { guard checkEnoughDiskSpaceAvailable() else { @@ -1060,19 +1016,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { return .lastAppLaunchCrashed } - if incrementalTSAttachmentMigrationStore.shouldReportFailureInUI() { - if let (logString, wasLoggedBefore) = incrementalTSAttachmentMigrationStore.consumeLastBGProcessingTaskError() { - if wasLoggedBefore { - Logger.error("Previously failed TSAttachment migration in some BGProcessingTask: \(logString)") - } else { - Logger.error("Failed TSAttachment migration in last BGProcessingTask: \(logString)") - } - } else if let checkpointString = incrementalTSAttachmentMigrationStore.getLastCheckpoint() { - Logger.error("Previously failed TSAttachment migration, last checkpoint: \(checkpointString)") - } - return .incrementalTSAttachmentMigrationFailed - } - return nil } @@ -1129,7 +1072,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { ) actions = [.submitDebugLogsAndCrash] - case .lastAppLaunchCrashed, .incrementalTSAttachmentMigrationFailed: + case .lastAppLaunchCrashed: title = OWSLocalizedString( "APP_LAUNCH_FAILURE_LAST_LAUNCH_CRASHED_TITLE", comment: "Error indicating that the app crashed during the previous launch." @@ -1271,7 +1214,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { func ignoreErrorAndLaunchApp(in window: UIWindow, launchContext: LaunchContext) { // Pretend we didn't fail! self.didAppLaunchFail = false - launchContext.incrementalMessageTSAttachmentMigrationStore.didReportFailureInUI() let loadingViewController = LoadingViewController() window.rootViewController = loadingViewController self.launchApp( diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index b33a30bb3f..b22cbd61fc 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -6,7 +6,6 @@ AttachmentValidationBackfillMigrator BackupBGProcessingTaskRunner - MessageAttachmentMigrationTask MessageFetchBGRefreshTask LazyDatabaseMigratorTask diff --git a/Signal/src/IncrementalMessageTSAttachmentMigrationRunner.swift b/Signal/src/IncrementalMessageTSAttachmentMigrationRunner.swift deleted file mode 100644 index ca75ea8064..0000000000 --- a/Signal/src/IncrementalMessageTSAttachmentMigrationRunner.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -import Foundation -import SignalServiceKit - -/// Manages the BGProcessingTask for doing the migration as well as the runner for -/// doing so while the main app is running. -class IncrementalMessageTSAttachmentMigrationRunner: BGProcessingTaskRunner { - private let appContext: AppContext - private let db: SDSDatabaseStorage - private let store: IncrementalTSAttachmentMigrationStore - private let migrator: () -> any IncrementalMessageTSAttachmentMigrator - - init( - appContext: AppContext, - db: SDSDatabaseStorage, - store: IncrementalTSAttachmentMigrationStore, - migrator: @escaping () -> any IncrementalMessageTSAttachmentMigrator - ) { - self.appContext = appContext - self.db = db - self.store = store - self.migrator = migrator - } - - // MARK: - BGProcessingTaskRunner - - static let taskIdentifier = "MessageAttachmentMigrationTask" - static let logPrefix: String? = nil - static let requiresNetworkConnectivity = false - static let requiresExternalPower = false - - func run() async throws { - let logger = MigrationLogger(appContext: appContext, store: store) - try await self.runInBatches( - willBegin: { store.willAttemptMigrationUntilFinished() }, - runNextBatch: { - return await migrator().runNextBatch(logger: logger) - } - ) - } - - public func startCondition() -> BGProcessingTaskStartCondition { - let state = db.read(block: store.getState(tx:)) - if state != .finished { - return .asSoonAsPossible - } else { - return .never - } - } - - private class MigrationLogger: TSAttachmentMigrationLogger { - - private let appContext: AppContext - private let store: IncrementalTSAttachmentMigrationStore - - init( - appContext: AppContext, - store: IncrementalTSAttachmentMigrationStore - ) { - self.appContext = appContext - self.store = store - } - - func didFatalError(_ logString: String) { - let logString = ScrubbingLogFormatter().redactMessage(logString) - store.bgProcessingTaskDidExperienceError(logString: logString) - } - - func flagDBCorrupted() { - DatabaseCorruptionState.flagDatabaseAsCorrupted(userDefaults: appContext.appUserDefaults()) - } - - func checkpoint(_ checkpointString: String) { - store.saveLastCheckpoint(checkpointString) - } - } -} diff --git a/SignalNSE/NSEEnvironment.swift b/SignalNSE/NSEEnvironment.swift index c4ae6e3c26..6bc84e6893 100644 --- a/SignalNSE/NSEEnvironment.swift +++ b/SignalNSE/NSEEnvironment.swift @@ -70,7 +70,6 @@ class NSEEnvironment { callMessageHandler: NSECallMessageHandler(), currentCallProvider: CurrentCallNoOpProvider(), notificationPresenter: NotificationPresenterImpl(), - incrementalMessageTSAttachmentMigratorFactory: NoOpIncrementalMessageTSAttachmentMigratorFactory(), ) .migrateDatabaseData() diff --git a/SignalServiceKit/Backups/Archiving/BackupArchiveManagerImpl.swift b/SignalServiceKit/Backups/Archiving/BackupArchiveManagerImpl.swift index 0917d10d75..01c8b9f39e 100644 --- a/SignalServiceKit/Backups/Archiving/BackupArchiveManagerImpl.swift +++ b/SignalServiceKit/Backups/Archiving/BackupArchiveManagerImpl.swift @@ -59,7 +59,6 @@ public class BackupArchiveManagerImpl: BackupArchiveManager { private let encryptedStreamProvider: BackupArchiveEncryptedProtoStreamProvider private let fullTextSearchIndexer: BackupArchiveFullTextSearchIndexer private let groupRecipientArchiver: BackupArchiveGroupRecipientArchiver - private let incrementalTSAttachmentMigrator: IncrementalMessageTSAttachmentMigrator private let kvStore: KeyValueStore private let libsignalNet: LibSignalClient.Net private let localStorage: AccountKeyStore @@ -101,7 +100,6 @@ public class BackupArchiveManagerImpl: BackupArchiveManager { encryptedStreamProvider: BackupArchiveEncryptedProtoStreamProvider, fullTextSearchIndexer: BackupArchiveFullTextSearchIndexer, groupRecipientArchiver: BackupArchiveGroupRecipientArchiver, - incrementalTSAttachmentMigrator: IncrementalMessageTSAttachmentMigrator, libsignalNet: LibSignalClient.Net, localStorage: AccountKeyStore, localRecipientArchiver: BackupArchiveLocalRecipientArchiver, @@ -139,7 +137,6 @@ public class BackupArchiveManagerImpl: BackupArchiveManager { self.encryptedStreamProvider = encryptedStreamProvider self.fullTextSearchIndexer = fullTextSearchIndexer self.groupRecipientArchiver = groupRecipientArchiver - self.incrementalTSAttachmentMigrator = incrementalTSAttachmentMigrator self.kvStore = KeyValueStore(collection: Constants.keyValueStoreCollectionName) self.libsignalNet = libsignalNet self.localStorage = localStorage @@ -382,14 +379,9 @@ public class BackupArchiveManagerImpl: BackupArchiveManager { DBReadTransaction ) -> BackupArchive.ProtoStream.OpenOutputStreamResult ) async throws -> OutputStreamMetadata { - let migrateAttachmentsProgressSink: OWSProgressSink? let prepareOversizeTextAttachmentsProgressSink: OWSProgressSink? let exportProgress: BackupArchiveExportProgress? if let progressSink { - migrateAttachmentsProgressSink = await progressSink.addChild( - withLabel: "Export Backup: Migrate Attachments", - unitCount: 5 - ) prepareOversizeTextAttachmentsProgressSink = await progressSink.addChild( withLabel: "Export Backup: Oversize Text Attachments", unitCount: 5 @@ -397,18 +389,15 @@ public class BackupArchiveManagerImpl: BackupArchiveManager { exportProgress = try await .prepare( sink: await progressSink.addChild( withLabel: "Export Backup: Export Frames", - unitCount: 90 + unitCount: 95 ), db: db ) } else { - migrateAttachmentsProgressSink = nil prepareOversizeTextAttachmentsProgressSink = nil exportProgress = nil } - await migrateAttachmentsBeforeBackup(progress: migrateAttachmentsProgressSink) - try await oversizeTextArchiver.populateTableIncrementally(progress: prepareOversizeTextAttachmentsProgressSink) // Before we export, we need to make sure we have an MRBK – the export @@ -847,19 +836,14 @@ public class BackupArchiveManagerImpl: BackupArchiveManager { DBReadTransaction ) -> BackupArchive.ProtoStream.OpenInputStreamResult ) async throws { - let migrateAttachmentsProgressSink: OWSProgressSink? let frameRestoreProgress: BackupArchiveImportFramesProgress? let recreateIndexesProgress: BackupArchiveImportRecreateIndexesProgress? let finalizeProgress: OWSProgressSink? if let progressSink { - migrateAttachmentsProgressSink = await progressSink.addChild( - withLabel: "Import Backup: Migrate Attachments", - unitCount: 5 - ) frameRestoreProgress = try await .prepare( sink: await progressSink.addChild( withLabel: "Import Backup: Import Frames", - unitCount: 78 + unitCount: 83 ), fileUrl: fileUrl ) @@ -874,14 +858,11 @@ public class BackupArchiveManagerImpl: BackupArchiveManager { unitCount: 5 ) } else { - migrateAttachmentsProgressSink = nil frameRestoreProgress = nil recreateIndexesProgress = nil finalizeProgress = nil } - await migrateAttachmentsBeforeBackup(progress: migrateAttachmentsProgressSink) - let backupInfo = try await db.awaitableWriteWithRollbackIfThrows { tx in return try BenchMemory( title: benchTitle, @@ -1469,27 +1450,6 @@ public class BackupArchiveManagerImpl: BackupArchiveManager { } } - /// TSAttachments must be migrated to v2 Attachments before we can create or restore backups. - /// Normally this migration happens in the background; force it to run and finish now. - private func migrateAttachmentsBeforeBackup(progress: OWSProgressSink?) async { - let didMigrateAnything = await incrementalTSAttachmentMigrator.runInMainAppUntilFinished( - ignorePastFailures: true, - progress: progress - ) - - if - let progress, - !didMigrateAnything - { - // Nothing was migrated, so progress wasn't updated. Complete it! - let source = await progress.addSource( - withLabel: "TSAttachmentMigrator had nothing to do", - unitCount: 1 - ) - source.complete() - } - } - private func validateEncryptedBackup( fileUrl: URL, backupEncryptionKey: MessageBackupKey, diff --git a/SignalServiceKit/Environment/AppSetup.swift b/SignalServiceKit/Environment/AppSetup.swift index b00e3b6c56..730823d3bf 100644 --- a/SignalServiceKit/Environment/AppSetup.swift +++ b/SignalServiceKit/Environment/AppSetup.swift @@ -179,7 +179,6 @@ extension AppSetup.GlobalsContinuation { callMessageHandler: CallMessageHandler, currentCallProvider: any CurrentCallProvider, notificationPresenter: any NotificationPresenter, - incrementalMessageTSAttachmentMigratorFactory: IncrementalMessageTSAttachmentMigratorFactory, testDependencies: AppSetup.TestDependencies = AppSetup.TestDependencies(), ) -> AppSetup.DataMigrationContinuation { configureUnsatisfiableConstraintLogging() @@ -1350,14 +1349,6 @@ extension AppSetup.GlobalsContinuation { usernameLookupManager: usernameLookupManager ) - let incrementalMessageTSAttachmentMigrator = incrementalMessageTSAttachmentMigratorFactory.migrator( - appContext: appContext, - appReadiness: appReadiness, - databaseStorage: databaseStorage, - remoteConfigManager: remoteConfigManager, - tsAccountManager: tsAccountManager - ) - let backupAttachmentsArchiver = BackupArchiveMessageAttachmentArchiver( attachmentManager: attachmentManager, attachmentStore: attachmentStore, @@ -1489,7 +1480,6 @@ extension AppSetup.GlobalsContinuation { storyStore: backupStoryStore, threadStore: backupThreadStore ), - incrementalTSAttachmentMigrator: incrementalMessageTSAttachmentMigrator, libsignalNet: libsignalNet, localStorage: accountKeyStore, localRecipientArchiver: BackupArchiveLocalRecipientArchiver( @@ -1696,7 +1686,6 @@ extension AppSetup.GlobalsContinuation { incomingCallEventSyncMessageManager: incomingCallEventSyncMessageManager, incomingCallLogEventSyncMessageManager: incomingCallLogEventSyncMessageManager, incomingPniChangeNumberProcessor: incomingPniChangeNumberProcessor, - incrementalMessageTSAttachmentMigrator: incrementalMessageTSAttachmentMigrator, individualCallRecordManager: individualCallRecordManager, interactionDeleteManager: interactionDeleteManager, interactionStore: interactionStore, diff --git a/SignalServiceKit/Environment/DependenciesBridge.swift b/SignalServiceKit/Environment/DependenciesBridge.swift index 10afdb381b..c4cec7ac22 100644 --- a/SignalServiceKit/Environment/DependenciesBridge.swift +++ b/SignalServiceKit/Environment/DependenciesBridge.swift @@ -122,7 +122,6 @@ public class DependenciesBridge { let incomingCallEventSyncMessageManager: IncomingCallEventSyncMessageManager let incomingCallLogEventSyncMessageManager: IncomingCallLogEventSyncMessageManager public let incomingPniChangeNumberProcessor: IncomingPniChangeNumberProcessor - public let incrementalMessageTSAttachmentMigrator: IncrementalMessageTSAttachmentMigrator public let individualCallRecordManager: IndividualCallRecordManager public let interactionDeleteManager: InteractionDeleteManager public let interactionStore: InteractionStore @@ -260,7 +259,6 @@ public class DependenciesBridge { incomingCallEventSyncMessageManager: IncomingCallEventSyncMessageManager, incomingCallLogEventSyncMessageManager: IncomingCallLogEventSyncMessageManager, incomingPniChangeNumberProcessor: IncomingPniChangeNumberProcessor, - incrementalMessageTSAttachmentMigrator: IncrementalMessageTSAttachmentMigrator, individualCallRecordManager: IndividualCallRecordManager, interactionDeleteManager: InteractionDeleteManager, interactionStore: InteractionStore, @@ -397,7 +395,6 @@ public class DependenciesBridge { self.incomingCallEventSyncMessageManager = incomingCallEventSyncMessageManager self.incomingCallLogEventSyncMessageManager = incomingCallLogEventSyncMessageManager self.incomingPniChangeNumberProcessor = incomingPniChangeNumberProcessor - self.incrementalMessageTSAttachmentMigrator = incrementalMessageTSAttachmentMigrator self.individualCallRecordManager = individualCallRecordManager self.interactionDeleteManager = interactionDeleteManager self.interactionStore = interactionStore diff --git a/SignalServiceKit/Environment/RemoteConfigManager.swift b/SignalServiceKit/Environment/RemoteConfigManager.swift index be11477a8b..f5f669e0c7 100644 --- a/SignalServiceKit/Environment/RemoteConfigManager.swift +++ b/SignalServiceKit/Environment/RemoteConfigManager.swift @@ -236,10 +236,6 @@ public class RemoteConfig { } } - public var tsAttachmentMigrationBatchDelayMs: UInt64 { - getUInt64Value(forFlag: .tsAttachmentMigrationBatchDelayMs, defaultValue: 50) - } - public var backupListMediaDefaultRefreshInterval: TimeInterval { let defaultValue: UInt64 if BuildFlags.Backups.useLowerDefaultListMediaRefreshInterval { @@ -287,14 +283,6 @@ public class RemoteConfig { return UInt64(messageQueueTime * Double(MSEC_PER_SEC)) } - public var shouldRunTSAttachmentMigrationInBGProcessingTask: Bool { - return !isEnabled(.tsAttachmentMigrationBGProcessingTaskKillSwitch) - } - - public var shouldRunTSAttachmentMigrationInMainAppBackground: Bool { - return !isEnabled(.tsAttachmentMigrationMainAppBackgroundKillSwitch) - } - public var backupSettingsKillSwitch: Bool { return isEnabled(.backupSettingsKillSwitch) } @@ -528,8 +516,6 @@ private enum IsEnabledFlag: String, FlagType { case pollReceiveKillSwitch = "ios.pollReceiveKillSwitch" case ringrtcNwPathMonitorTrialKillSwitch = "ios.ringrtcNwPathMonitorTrialKillSwitch" case serviceExtensionFailureKillSwitch = "ios.serviceExtensionFailureKillSwitch" - case tsAttachmentMigrationBGProcessingTaskKillSwitch = "ios.tsAttachmentMigrationBGProcessingTaskKillSwitch" - case tsAttachmentMigrationMainAppBackgroundKillSwitch = "ios.tsAttachmentMigrationMainAppBackgroundKillSwitch" #if TESTABLE_BUILD case hotSwappable = "test.hotSwappable.enabled" @@ -557,8 +543,6 @@ private enum IsEnabledFlag: String, FlagType { case .pollReceiveKillSwitch: true case .ringrtcNwPathMonitorTrialKillSwitch: true // cached during launch, so not hot-swapped in practice case .serviceExtensionFailureKillSwitch: true - case .tsAttachmentMigrationBGProcessingTaskKillSwitch: true - case .tsAttachmentMigrationMainAppBackgroundKillSwitch: true #if TESTABLE_BUILD case .hotSwappable: true @@ -592,7 +576,6 @@ private enum ValueFlag: String, FlagType { case replaceableInteractionExpiration = "ios.replaceableInteractionExpiration" case sepaEnabledRegions = "global.donations.sepaEnabledRegions" case standardMediaQualityLevel = "ios.standardMediaQualityLevel" - case tsAttachmentMigrationBatchDelayMs = "ios.tsAttachmentMigrationBatchDelayMs" case backupListMediaDefaultRefreshIntervalMs = "ios.backupListMediaDefaultRefreshIntervalMs" case backupListMediaOutOfQuotaRefreshIntervalMs = "ios.backupListMediaOutOfQuotaRefreshIntervalMs" case pinnedMessageLimit = "global.pinned_message_limit" @@ -627,7 +610,6 @@ private enum ValueFlag: String, FlagType { case .replaceableInteractionExpiration: false case .sepaEnabledRegions: true case .standardMediaQualityLevel: false - case .tsAttachmentMigrationBatchDelayMs: true case .backupListMediaDefaultRefreshIntervalMs: true case .backupListMediaOutOfQuotaRefreshIntervalMs: true case .pinnedMessageLimit: true diff --git a/SignalServiceKit/Resources/schema.sql b/SignalServiceKit/Resources/schema.sql index b566f2b017..02d63fab3b 100644 --- a/SignalServiceKit/Resources/schema.sql +++ b/SignalServiceKit/Resources/schema.sql @@ -2048,78 +2048,6 @@ CREATE ) ; -CREATE - TABLE - IF NOT EXISTS "model_TSAttachment" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL - ,"recordType" INTEGER NOT NULL - ,"uniqueId" TEXT NOT NULL UNIQUE - ON CONFLICT FAIL - ,"albumMessageId" TEXT - ,"attachmentType" INTEGER NOT NULL - ,"blurHash" TEXT - ,"byteCount" INTEGER NOT NULL - ,"caption" TEXT - ,"contentType" TEXT NOT NULL - ,"encryptionKey" BLOB - ,"serverId" INTEGER NOT NULL - ,"sourceFilename" TEXT - ,"cachedAudioDurationSeconds" DOUBLE - ,"cachedImageHeight" DOUBLE - ,"cachedImageWidth" DOUBLE - ,"creationTimestamp" DOUBLE - ,"digest" BLOB - ,"isUploaded" INTEGER - ,"isValidImageCached" INTEGER - ,"isValidVideoCached" INTEGER - ,"lazyRestoreFragmentId" TEXT - ,"localRelativeFilePath" TEXT - ,"mediaSize" BLOB - ,"pointerType" INTEGER - ,"state" INTEGER - ,"uploadTimestamp" INTEGER NOT NULL DEFAULT 0 - ,"cdnKey" TEXT NOT NULL DEFAULT '' - ,"cdnNumber" INTEGER NOT NULL DEFAULT 0 - ,"isAnimatedCached" INTEGER - ,"attachmentSchemaVersion" INTEGER DEFAULT 0 - ,"videoDuration" DOUBLE - ,"clientUuid" TEXT - ) -; - -CREATE - INDEX "index_model_TSAttachment_on_uniqueId_and_contentType" - ON "model_TSAttachment"("uniqueId" - ,"contentType" -) -; - -CREATE - TABLE - IF NOT EXISTS "TSAttachmentMigration" ( - "tsAttachmentUniqueId" TEXT NOT NULL - ,"interactionRowId" INTEGER - ,"storyMessageRowId" INTEGER - ,"reservedV2AttachmentPrimaryFileId" BLOB NOT NULL - ,"reservedV2AttachmentAudioWaveformFileId" BLOB NOT NULL - ,"reservedV2AttachmentVideoStillFrameFileId" BLOB NOT NULL - ) -; - -CREATE - INDEX "index_TSAttachmentMigration_on_interactionRowId" - ON "TSAttachmentMigration" ("interactionRowId") -WHERE - "interactionRowId" IS NOT NULL -; - -CREATE - INDEX "index_TSAttachmentMigration_on_storyMessageRowId" - ON "TSAttachmentMigration" ("storyMessageRowId") -WHERE - "storyMessageRowId" IS NOT NULL -; - CREATE TABLE IF NOT EXISTS "BlockedGroup" ( diff --git a/SignalServiceKit/SignalServiceKit.h b/SignalServiceKit/SignalServiceKit.h index 83647cfe02..00ff70a28d 100644 --- a/SignalServiceKit/SignalServiceKit.h +++ b/SignalServiceKit/SignalServiceKit.h @@ -11,7 +11,6 @@ FOUNDATION_EXPORT double SignalServiceKitVersionNumber; //! Project version string for SignalServiceKit. FOUNDATION_EXPORT const unsigned char SignalServiceKitVersionString[]; -#import #import #import #import diff --git a/SignalServiceKit/Storage/Database/DatabaseRecovery.swift b/SignalServiceKit/Storage/Database/DatabaseRecovery.swift index 9f97d4a65e..33b24979a1 100644 --- a/SignalServiceKit/Storage/Database/DatabaseRecovery.swift +++ b/SignalServiceKit/Storage/Database/DatabaseRecovery.swift @@ -301,8 +301,6 @@ public extension DatabaseRecovery { QueuedBackupStickerPackDownload.databaseTableName, OrphanedBackupAttachment.databaseTableName, "MessageBackupAvatarFetchQueue", - "model_TSAttachment", - "TSAttachmentMigration", "AvatarDefaultColor", GroupMessageProcessorJob.databaseTableName, "ListedBackupMediaObject", diff --git a/SignalServiceKit/Storage/Database/GRDBSchemaMigrator.swift b/SignalServiceKit/Storage/Database/GRDBSchemaMigrator.swift index 92cad6eebb..1648243892 100644 --- a/SignalServiceKit/Storage/Database/GRDBSchemaMigrator.swift +++ b/SignalServiceKit/Storage/Database/GRDBSchemaMigrator.swift @@ -255,12 +255,7 @@ public class GRDBSchemaMigrator { case createArchivedPaymentTable case removeDeadEndGroupThreadIdMappings case addTSAttachmentMigrationTable - case threadWallpaperTSAttachmentMigration1 - case threadWallpaperTSAttachmentMigration2 - case threadWallpaperTSAttachmentMigration3 case indexMessageAttachmentReferenceByReceivedAtTimestamp - case migrateStoryMessageTSAttachments1 - case migrateStoryMessageTSAttachments2 case addBackupAttachmentDownloadQueue case createAttachmentUploadRecordTable case addBlockedRecipient @@ -337,6 +332,7 @@ public class GRDBSchemaMigrator { case fixNameForRestoredCallLinks case addPinnedMessagesTable case addPinnedAtTimestampToPinnedMessageTable + case dropTSAttachment // NOTE: Every time we add a migration id, consider // incrementing grdbSchemaVersionLatest. @@ -448,6 +444,13 @@ public class GRDBSchemaMigrator { // Obsoleted by dataMigration_removeSystemContacts. case dataMigration_removeLinkedDeviceSystemContacts + + // Obsoleted when we removed the TSAttachment migration. + case threadWallpaperTSAttachmentMigration1 + case threadWallpaperTSAttachmentMigration2 + case threadWallpaperTSAttachmentMigration3 + case migrateStoryMessageTSAttachments1 + case migrateStoryMessageTSAttachments2 #endif } @@ -3067,21 +3070,6 @@ public class GRDBSchemaMigrator { return .success(()) } - migrator.registerMigration(.threadWallpaperTSAttachmentMigration1) { tx in - try TSAttachmentMigration.prepareThreadWallpaperMigration(tx: tx) - return .success(()) - } - - migrator.registerMigration(.threadWallpaperTSAttachmentMigration2) { tx in - try TSAttachmentMigration.completeThreadWallpaperMigration(tx: tx) - return .success(()) - } - - migrator.registerMigration(.threadWallpaperTSAttachmentMigration3) { tx in - try TSAttachmentMigration.cleanUpLegacyThreadWallpaperDirectory() - return .success(()) - } - migrator.registerMigration(.indexMessageAttachmentReferenceByReceivedAtTimestamp) { tx in try tx.database.create( index: "index_message_attachment_reference_on_receivedAtTimestamp", @@ -3091,16 +3079,6 @@ public class GRDBSchemaMigrator { return .success(()) } - migrator.registerMigration(.migrateStoryMessageTSAttachments1) { tx in - try TSAttachmentMigration.StoryMessageMigration.prepareStoryMessageMigration(tx: tx) - return .success(()) - } - - migrator.registerMigration(.migrateStoryMessageTSAttachments2) { tx in - try TSAttachmentMigration.StoryMessageMigration.completeStoryMessageMigration(tx: tx) - return .success(()) - } - migrator.registerMigration(.addBackupAttachmentDownloadQueue) { tx in try tx.database.create(table: "BackupAttachmentDownloadQueue") { table in table.autoIncrementedPrimaryKey("id") @@ -4326,6 +4304,22 @@ public class GRDBSchemaMigrator { return .success(()) } + migrator.registerMigration(.dropTSAttachment) { tx in + // Delete the legacy attachments folder, which is hopefully already + // deleted. + if let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: TSConstants.applicationGroup, + ) { + let tsAttachmentsDir = containerURL.path.appendingPathComponent("Attachments") + try? FileManager.default.removeItem(atPath: tsAttachmentsDir) + } + + try tx.database.drop(table: "TSAttachmentMigration") + try tx.database.drop(table: "model_TSAttachment") + + return .success(()) + } + // MARK: - Schema Migration Insertion Point } diff --git a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/AVAssetReaderTrackOutputWrapper.h b/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/AVAssetReaderTrackOutputWrapper.h deleted file mode 100644 index f7958215f0..0000000000 --- a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/AVAssetReaderTrackOutputWrapper.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright 2025 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface AVAssetReaderTrackOutputWrapper : NSObject - -/// Safely creates an AVAssetReaderTrackOutput instance. Returns nil if creation fails. -+ (nullable AVAssetReaderTrackOutput *)safeAssetReaderTrackOutputWithTrack:(AVAssetTrack *)track - outputSettings: - (nullable NSDictionary *)outputSettings; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/AVAssetReaderTrackOutputWrapper.m b/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/AVAssetReaderTrackOutputWrapper.m deleted file mode 100644 index cf9ea3cd9a..0000000000 --- a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/AVAssetReaderTrackOutputWrapper.m +++ /dev/null @@ -1,24 +0,0 @@ -// -// Copyright 2025 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -#import "AVAssetReaderTrackOutputWrapper.h" - -@implementation AVAssetReaderTrackOutputWrapper - -+ (nullable AVAssetReaderTrackOutput *)safeAssetReaderTrackOutputWithTrack:(AVAssetTrack *)track - outputSettings: - (nullable NSDictionary *)outputSettings -{ - @try { - AVAssetReaderTrackOutput *_Nullable output = - [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:track outputSettings:outputSettings]; - return output; - } @catch (NSException *exception) { - OWSFailDebug(@"Unable to generate AVAssetReaderTrackOutput: %@", exception); - return nil; - } -} - -@end diff --git a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/IncrementalMessageTSAttachmentMigrator.swift b/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/IncrementalMessageTSAttachmentMigrator.swift deleted file mode 100644 index 6d6908985e..0000000000 --- a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/IncrementalMessageTSAttachmentMigrator.swift +++ /dev/null @@ -1,318 +0,0 @@ -// -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -import Foundation - -/// Incrementally migrates TSAttachments owned by TSMessages to v2 attachments. -public protocol IncrementalMessageTSAttachmentMigrator { - - /// - parameter ignorePastFailures: If true, will always run regardless of past failures. - /// If false, will skip running if previous attempts failed. - /// - /// Supports task cancellation. - /// - /// - Returns - /// True if anything was migrated, false otherwise. - @discardableResult - func runInMainAppUntilFinished(ignorePastFailures: Bool, progress: OWSProgressSink?) async -> Bool - - // Returns true if done. - func runNextBatch(logger: TSAttachmentMigrationLogger) async -> Bool -} - -public class IncrementalMessageTSAttachmentMigratorImpl: IncrementalMessageTSAttachmentMigrator { - - private let appContext: AppContext - private let databaseStorage: SDSDatabaseStorage - private let remoteConfigManager: RemoteConfigManager - private let store: IncrementalTSAttachmentMigrationStore - private let tsAccountManager: TSAccountManager - - public init( - appContext: AppContext, - appReadiness: AppReadiness, - databaseStorage: SDSDatabaseStorage, - remoteConfigManager: RemoteConfigManager, - store: IncrementalTSAttachmentMigrationStore, - tsAccountManager: TSAccountManager - ) { - self.appContext = appContext - self.databaseStorage = databaseStorage - self.remoteConfigManager = remoteConfigManager - self.store = store - self.tsAccountManager = tsAccountManager - - appReadiness.runNowOrWhenAppDidBecomeReadyAsync { [weak self] in - guard let self else { return } - NotificationCenter.default.addObserver( - self, - selector: #selector(appDidBecomeActive), - name: .OWSApplicationDidBecomeActive, - object: nil - ) - Task { await self.runInMainAppBackground() } - } - } - - @objc - private func appDidBecomeActive() { - Task { await self.runInMainAppBackground() } - } - - public func runInMainAppUntilFinished(ignorePastFailures: Bool, progress: OWSProgressSink?) async -> Bool { - // We DO NOT check any of the feature flag or remote config break-glass-es here; - // this is used by backups which require the migration to have finished - // and aren't enabled outside internal builds anyway. - - if !ignorePastFailures, !store.shouldAttemptMigrationUntilFinished() { - Logger.warn("Skipping migration because of past failed attempts") - return false - } - - let state = databaseStorage.read(block: store.getState(tx:)) - switch state { - case .finished: - return false - case .unstarted, .started: - Logger.info("Running until finished") - } - - var remainingAttachmentCount: UInt64 = 0 - var progressSource: OWSProgressSource? - if let progress { - remainingAttachmentCount = databaseStorage.read(block: fetchRemainingTSAttachmentCount(tx:)) - if remainingAttachmentCount > 0 { - progressSource = await progress.addSource( - withLabel: "Remaining Interactions", - unitCount: remainingAttachmentCount - ) - } - } - - store.willAttemptMigrationUntilFinished() - - let logger = MainAppMigrationLogger(appContext: appContext, store: store) - - var batchCount = 0 - var didFinish = false - while !didFinish { - // Run in batches, instead of one big write transaction, so that - // we can commit incremental progress if we are interrupted. - didFinish = await self.runNextBatch(logger: logger) - batchCount += 1 - - if let progressSource { - let newCount = databaseStorage.read(block: fetchRemainingTSAttachmentCount(tx:)) - let diff = remainingAttachmentCount - newCount - remainingAttachmentCount = newCount - if diff > 0 { - progressSource.incrementCompletedUnitCount(by: diff) - } - } - - do { - try Task.checkCancellation() - } catch { - Logger.warn("Cancelled; stopping after \(batchCount) batches") - return true - } - } - - Logger.info("Ran until finished after \(batchCount) batches") - return true - } - - private func fetchRemainingTSAttachmentCount(tx: DBReadTransaction) -> UInt64 { - UInt64(clamping: (try? Int64.fetchOne( - tx.database, - sql: "SELECT COUNT(id) FROM model_TSAttachment;" - )) ?? 0) - } - - private let isRunningInMainApp = AtomicBool(false, lock: .init()) - - private func runInMainAppBackground() async { - guard - BuildFlags.runTSAttachmentMigrationInMainAppBackground, - appContext.isMainAppAndActive, - isRunningInMainApp.tryToSetFlag() - else { - return - } - defer { - isRunningInMainApp.set(false) - } - let state = databaseStorage.read(block: store.getState(tx:)) - switch state { - case .unstarted: - Logger.info("Has not started message attachment migration") - case .started: - Logger.info("Partial progress on message attachment migration") - case .finished: - Logger.info("Finished message attachment migration") - return - } - - // Fetch remote config for kill switch; if fetch fails use cached local config. - let isAllowedByRemoteConfig: Bool - if tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered { - try? await remoteConfigManager.refreshIfNeeded() - let remoteConfig = remoteConfigManager.currentConfig() - isAllowedByRemoteConfig = remoteConfig.shouldRunTSAttachmentMigrationInMainAppBackground - } else { - // If we aren't registered, we can't fetch a remote config. - // Use default true, even if we have a cached remote config value. - // We want to try running; worst case these deregistered users fail/crash once - // and then are prevented from crashing further by the attempt counting. - isAllowedByRemoteConfig = true - } - guard isAllowedByRemoteConfig else { - Logger.info("Disabled via remote config, stopping") - return - } - - if !store.shouldAttemptMigrationUntilFinished() { - Logger.warn("Skipping background migration because of past failed attempts") - return - } - - let delayMs = remoteConfigManager.currentConfig().tsAttachmentMigrationBatchDelayMs - - store.willAttemptMigrationUntilFinished() - - let logger = MainAppMigrationLogger(appContext: appContext, store: store) - - var batchCount = 0 - var didFinish = false - while !didFinish { - // Add a small delay between each batch to avoid locking the db write queue. - try? await Task.sleep(nanoseconds: delayMs * NSEC_PER_MSEC) - - guard appContext.isMainAppAndActive else { - // If the main app goes into the background, we shouldn't be - // grabbing the sql write lock. Stop. - Logger.info("Stopping when backgrounding app after \(batchCount) batches") - if batchCount == 0 { - // If we exit before doing a single batch, don't count it as a failure. - store.didEarlyExitBeforeAttemptingBatch() - } - return - } - - // Only migrate one message at a time so we don't hold the write lock - // too long while doing file i/o. - didFinish = await self._runNextBatch(messageBatchSize: 1, logger: logger) - batchCount += 1 - } - Logger.info("Finished in main app after \(batchCount) batches") - } - - // Returns true if done. - public func runNextBatch(logger: TSAttachmentMigrationLogger) async -> Bool { - return await _runNextBatch(logger: logger) - } - - // Returns true if done. - private func _runNextBatch(messageBatchSize: Int = 5, logger: TSAttachmentMigrationLogger) async -> Bool { - typealias Migrator = TSAttachmentMigration.TSMessageMigration - - let isDone = await databaseStorage.awaitableWrite { tx in - // First we try to migrate a batch of prepared messages. - let didMigrateBatch = Migrator.completeNextIterativeTSMessageMigrationBatch( - batchSize: messageBatchSize, - logger: logger, - tx: tx - ) - if didMigrateBatch { - return false - } - - // If no messages are prepared, we try to prepare a batch of messages. - let didPrepareBatch = Migrator.prepareNextIterativeTSMessageMigrationBatch( - logger: logger, - tx: tx - ) - if didPrepareBatch { - do { - try self.store.setState(.started, tx: tx) - } catch let error { - logger.didFatalError("\(error)") - owsFail("Failed to write state to db") - } - return false - } - - // If there was nothing to migrate and nothing to prepare, wipe the files and finish. - Migrator.cleanUpTSAttachmentFiles() - do { - try self.store.setState(.finished, tx: tx) - } catch let error { - logger.didFatalError("\(error)") - owsFail("Failed to write state to db") - } - return true - } - store.didSucceedMigrationBatch() - return isDone - } - - private class MainAppMigrationLogger: TSAttachmentMigrationLogger { - - private let appContext: AppContext - private let store: IncrementalTSAttachmentMigrationStore - - init( - appContext: AppContext, - store: IncrementalTSAttachmentMigrationStore - ) { - self.appContext = appContext - self.store = store - } - - func didFatalError(_ logString: String) { - // In this context we don't do anything with errors; - // the owsFail is in the same process. - } - - func flagDBCorrupted() { - DatabaseCorruptionState.flagDatabaseAsCorrupted(userDefaults: appContext.appUserDefaults()) - } - - func checkpoint(_ checkpointString: String) { - store.saveLastCheckpoint(checkpointString) - } - } -} - -public class NoOpIncrementalMessageTSAttachmentMigrator: IncrementalMessageTSAttachmentMigrator { - public init() {} - - public func runInMainAppUntilFinished(ignorePastFailures: Bool, progress: OWSProgressSink?) async -> Bool { - return false - } - - // Returns true if done. - public func runNextBatch(logger: TSAttachmentMigrationLogger) async -> Bool { - return true - } -} - -#if TESTABLE_BUILD - -public class IncrementalMessageTSAttachmentMigratorMock: IncrementalMessageTSAttachmentMigrator { - - public init() {} - - public func runInMainAppUntilFinished(ignorePastFailures: Bool, progress: OWSProgressSink?) async -> Bool { - return false - } - - // Returns true if done. - public func runNextBatch(logger: TSAttachmentMigrationLogger) async -> Bool { - return true - } -} - -#endif diff --git a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/IncrementalMessageTSAttachmentMigratorFactory.swift b/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/IncrementalMessageTSAttachmentMigratorFactory.swift deleted file mode 100644 index e78c373e41..0000000000 --- a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/IncrementalMessageTSAttachmentMigratorFactory.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -public protocol IncrementalMessageTSAttachmentMigratorFactory { - - func migrator( - appContext: AppContext, - appReadiness: AppReadiness, - databaseStorage: SDSDatabaseStorage, - remoteConfigManager: RemoteConfigManager, - tsAccountManager: TSAccountManager - ) -> IncrementalMessageTSAttachmentMigrator -} - -public class IncrementalMessageTSAttachmentMigratorFactoryImpl: IncrementalMessageTSAttachmentMigratorFactory { - - private let store: IncrementalTSAttachmentMigrationStore - - public init(store: IncrementalTSAttachmentMigrationStore) { - self.store = store - } - - public func migrator( - appContext: AppContext, - appReadiness: AppReadiness, - databaseStorage: SDSDatabaseStorage, - remoteConfigManager: RemoteConfigManager, - tsAccountManager: TSAccountManager - ) -> IncrementalMessageTSAttachmentMigrator { - return IncrementalMessageTSAttachmentMigratorImpl( - appContext: appContext, - appReadiness: appReadiness, - databaseStorage: databaseStorage, - remoteConfigManager: remoteConfigManager, - store: store, - tsAccountManager: tsAccountManager - ) - } -} - -public class NoOpIncrementalMessageTSAttachmentMigratorFactory: IncrementalMessageTSAttachmentMigratorFactory { - - public init() {} - - public func migrator( - appContext: AppContext, - appReadiness: AppReadiness, - databaseStorage: SDSDatabaseStorage, - remoteConfigManager: RemoteConfigManager, - tsAccountManager: TSAccountManager - ) -> IncrementalMessageTSAttachmentMigrator { - return NoOpIncrementalMessageTSAttachmentMigrator() - } -} - -#if TESTABLE_BUILD - -public class IncrementalMessageTSAttachmentMigratorFactoryMock: IncrementalMessageTSAttachmentMigratorFactory { - - public init() {} - - public func migrator( - appContext: AppContext, - appReadiness: AppReadiness, - databaseStorage: SDSDatabaseStorage, - remoteConfigManager: RemoteConfigManager, - tsAccountManager: TSAccountManager - ) -> IncrementalMessageTSAttachmentMigrator { - return IncrementalMessageTSAttachmentMigratorMock() - } -} - -#endif diff --git a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/IncrementalTSAttachmentMigrationStore.swift b/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/IncrementalTSAttachmentMigrationStore.swift deleted file mode 100644 index e727d8df14..0000000000 --- a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/IncrementalTSAttachmentMigrationStore.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -import Foundation - -public class IncrementalTSAttachmentMigrationStore { - - public enum State: Int, Codable { - case unstarted - case started - case finished - - static let key = "state" - } - - private let userDefaults: UserDefaults - - public init(userDefaults: UserDefaults) { - self.userDefaults = userDefaults - } - - private let kvStore = KeyValueStore(collection: "IncrementalMessageTSAttachmentMigrator") - - public func getState(tx: DBReadTransaction) -> State { - return (try? kvStore.getCodableValue(forKey: State.key, transaction: tx)) ?? .unstarted - } - - public func setState(_ state: State, tx: DBWriteTransaction) throws { - try kvStore.setCodable(state, key: State.key, transaction: tx) - } - - // MARK: - Errors - - /// Value should be incremented when we apply a forward fix to the TSAttachment migration. - /// When this value changes, users who had previously failed to migrate and are now skipping the migration - /// will attempt to migrate once again. - /// v1 = initial launch - /// v2 = bug where we'd count migrations that got interrupted by app termination before finishing one batch - /// as "failed"; we increment the number so they retry now that this is resolved. - /// v3 = a couple failures that mostly look like db corruption; added db corruption checks. - private static let currentMigrationVersion = 3 - /// NOTE: in reality one more attempt than this number may happen before we give up. This is because - /// we count an attempt as successful if it completes a single batch; if a subsequent batch fails the attempt - /// was still counted as success so we will try again (with the failing batch now being the first batch) and - /// a second failure in the now-first batch will count as a failed attempt and prevent future attempts. - private static let maxNumAttemptsBeforeSkipping = 2 - private static let lastMigrationAttemptVersionKey = "TSAttachmentMigration_lastMigrationAttemptVersionKey" - private static let lastMigrationAttemptDateKey = "TSAttachmentMigration_lastMigrationAttemptDateKey" - private static let migrationIncompleteAttemptCountKey = "TSAttachmentMigration_migrationIncompleteAttemptCountKey" - private static let didReportFailureInUIKey = "TSAttachmentMigration_didReportFailureInUIKey" - - public func shouldAttemptMigrationUntilFinished() -> Bool { - let lastAttemptVersion = userDefaults.integer(forKey: Self.lastMigrationAttemptVersionKey) - if lastAttemptVersion != Self.currentMigrationVersion { - return true - } - let lastAttemptDate: Date? = userDefaults.object(forKey: Self.lastMigrationAttemptDateKey) as? Date - if Date().timeIntervalSince((lastAttemptDate ?? .distantPast)) >= .week { - return true - } - let incompleteAttemptCount = userDefaults.integer(forKey: Self.migrationIncompleteAttemptCountKey) - return incompleteAttemptCount < Self.maxNumAttemptsBeforeSkipping - } - - public func willAttemptMigrationUntilFinished() { - let lastAttemptVersion = userDefaults.integer(forKey: Self.lastMigrationAttemptVersionKey) - let prevIncompleteAttemptCount: Int - if lastAttemptVersion == Self.currentMigrationVersion { - prevIncompleteAttemptCount = userDefaults.integer(forKey: Self.migrationIncompleteAttemptCountKey) - } else { - prevIncompleteAttemptCount = 0 - } - userDefaults.set(Self.currentMigrationVersion, forKey: Self.lastMigrationAttemptVersionKey) - userDefaults.set(Date(), forKey: Self.lastMigrationAttemptDateKey) - userDefaults.set(prevIncompleteAttemptCount + 1, forKey: Self.migrationIncompleteAttemptCountKey) - userDefaults.set(false, forKey: Self.didReportFailureInUIKey) - } - - public func didSucceedMigrationBatch() { - userDefaults.set(0, forKey: Self.migrationIncompleteAttemptCountKey) - userDefaults.set(Date(), forKey: Self.lastMigrationAttemptDateKey) - userDefaults.set(false, forKey: Self.didReportFailureInUIKey) - } - - public func didEarlyExitBeforeAttemptingBatch() { - let prevIncompleteAttemptCount = userDefaults.integer(forKey: Self.migrationIncompleteAttemptCountKey) - guard prevIncompleteAttemptCount > 0 else { - owsFailDebug("Not marked as making an attempt") - return - } - userDefaults.set(prevIncompleteAttemptCount - 1, forKey: Self.migrationIncompleteAttemptCountKey) - } - - public func shouldReportFailureInUI() -> Bool { - if userDefaults.bool(forKey: Self.didReportFailureInUIKey) { - return false - } - return !shouldAttemptMigrationUntilFinished() - } - - public func didReportFailureInUI() { - userDefaults.set(true, forKey: Self.didReportFailureInUIKey) - } - - // MARK: BGProcessingTask - - private static let bgProcessingTaskErrorKey = "TSAttachmentMigration_bgProcessingTaskErrorKey" - private static let hasLoggedBgProcessingTaskErrorKey = "TSAttachmentMigration_hasLoggedBGProcessingTaskErrorKey" - - public func bgProcessingTaskDidExperienceError(logString: String) { - userDefaults.set(logString, forKey: Self.bgProcessingTaskErrorKey) - userDefaults.setValue(false, forKey: Self.hasLoggedBgProcessingTaskErrorKey) - } - - /// Returns (error string, has been logged before) - public func consumeLastBGProcessingTaskError() -> (String, Bool)? { - let value = userDefaults.string(forKey: Self.bgProcessingTaskErrorKey) - guard let value else { return nil } - let wasLoggedBefore = userDefaults.bool(forKey: Self.hasLoggedBgProcessingTaskErrorKey) - if !wasLoggedBefore { - userDefaults.setValue(true, forKey: Self.hasLoggedBgProcessingTaskErrorKey) - } - return (value, wasLoggedBefore) - } - - // MARK: Checkpoints - - private static let lastCheckpointKey = "TSAttachmentMigration_lastCheckpointKey" - - public func saveLastCheckpoint(_ checkpointString: String) { - userDefaults.set(checkpointString, forKey: Self.lastCheckpointKey) - } - - public func getLastCheckpoint() -> String? { - userDefaults.string(forKey: Self.lastCheckpointKey) - } -} diff --git a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+AttachmentValidator.swift b/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+AttachmentValidator.swift deleted file mode 100644 index 322c1dbe37..0000000000 --- a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+AttachmentValidator.swift +++ /dev/null @@ -1,693 +0,0 @@ -// -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -import AVKit -import Foundation - -extension TSAttachmentMigration { - - struct PendingV2AttachmentFile { - let blurHash: String? - let sha256ContentHash: Data - let encryptedByteCount: UInt32 - let unencryptedByteCount: UInt32 - let mimeType: String - let encryptionKey: Data - let digestSHA256Ciphertext: Data - let localRelativeFilePath: String - let renderingFlag: TSAttachmentMigration.V2RenderingFlag - let sourceFilename: String? - let validatedContentType: TSAttachmentMigration.V2Attachment.ContentType - let audioDurationSeconds: Double? - let mediaSizePixels: CGSize? - let videoDurationSeconds: Double? - let audioWaveformRelativeFilePath: String? - let videoStillFrameRelativeFilePath: String? - } - - class V2AttachmentContentValidator { - - // Note that unlike "live" attachment validation which assigns final - // attachment file locations on the fly, the migrations are required - // to "reserve" the final location using a random but persisted UUID. - // This way if the migration is interrupted, any files we managed - // to create before interruption are simply written over instead of - // living forever unreferenced and consuming space. - struct ReservedRelativeFileIds { - let primaryFile: UUID - let audioWaveform: UUID - let videoStillFrame: UUID - } - - static func validateContents( - unencryptedFileUrl: URL, - reservedFileIds: ReservedRelativeFileIds, - attachmentKey: AttachmentKey? = nil, - mimeType: String, - renderingFlag: TSAttachmentMigration.V2RenderingFlag, - sourceFilename: String? - ) throws -> TSAttachmentMigration.PendingV2AttachmentFile { - let byteSize: UInt64 = { - return (try? OWSFileSystem.fileSize(of: unencryptedFileUrl)) ?? 0 - }() - guard byteSize < 95 * 1000 * 1000 /* SignalAttachment.kMaxFileSizeGeneric */ else { - throw AttachmentTooLargeError() - } - - let attachmentKey = attachmentKey ?? .generate() - let pendingAttachment = try validateContents( - unencryptedFileUrl: unencryptedFileUrl, - byteSize: Int(byteSize), - reservedFileIds: reservedFileIds, - attachmentKey: attachmentKey, - mimeType: mimeType, - renderingFlag: renderingFlag, - sourceFilename: sourceFilename - ) - - return pendingAttachment - } - - private static func validateContents( - unencryptedFileUrl: URL, - byteSize: Int, - reservedFileIds: ReservedRelativeFileIds, - attachmentKey: AttachmentKey, - mimeType: String, - renderingFlag: TSAttachmentMigration.V2RenderingFlag, - sourceFilename: String? - ) throws -> TSAttachmentMigration.PendingV2AttachmentFile { - var mimeType = mimeType - let contentTypeResult = try validateContentType( - unencryptedFileUrl: unencryptedFileUrl, - byteSize: byteSize, - reservedFileIds: reservedFileIds, - attachmentKey: attachmentKey, - mimeType: &mimeType - ) - return try prepareAttachmentFiles( - unencryptedFileUrl, - reservedFileIds: reservedFileIds, - attachmentKey: attachmentKey, - mimeType: mimeType, - renderingFlag: renderingFlag, - sourceFilename: sourceFilename, - contentResult: contentTypeResult - ) - } - - private static let thumbnailDimensionPointsForQuotedReply: CGFloat = 200 - - static func prepareQuotedReplyThumbnail( - fromOriginalAttachmentStream stream: TSAttachmentMigration.V1Attachment, - reservedFileIds: ReservedRelativeFileIds, - renderingFlag: TSAttachmentMigration.V2RenderingFlag, - sourceFilename: String? - ) throws -> TSAttachmentMigration.PendingV2AttachmentFile? { - guard let localFilePath = stream.localFilePath else { - throw OWSAssertionError("Non stream") - } - - let originalImage: UIImage - - // The thing called "contentType" on TSAttachment is the MIME type. - let contentType = self.rawContentType(mimeType: stream.contentType) - switch contentType { - case .invalid, .audio, .file: - throw OWSAssertionError("Non visual media target") - case .image, .animatedImage: - guard let image = UIImage(contentsOfFile: localFilePath) else { - Logger.error("Unable to read image") - return nil - } - originalImage = image - case .video: - let asset: AVAsset = AVAsset(url: URL(fileURLWithPath: localFilePath)) - - guard TSAttachmentMigration.OWSMediaUtils.isValidVideo(asset: asset) else { - throw OWSAssertionError("Unable to read video") - } - - do { - originalImage = try TSAttachmentMigration.OWSMediaUtils.thumbnail( - forVideo: asset, - maxSizePixels: .square(AttachmentThumbnailQuality.large.thumbnailDimensionPoints()) - ) - } catch { - Logger.error("Failed to generate video still frame") - return nil - } - } - - guard - let resizedImage = TSAttachmentMigration.OWSMediaUtils.resize( - image: originalImage, - maxDimensionPoints: Self.thumbnailDimensionPointsForQuotedReply - ), - let imageData = resizedImage.jpegData(compressionQuality: 0.8) - else { - Logger.error("Unable to create thumbnail") - return nil - } - - let tmpFile = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true) - try imageData.write(to: tmpFile) - - let renderingFlagForThumbnail: TSAttachmentMigration.V2RenderingFlag - switch renderingFlag { - case .borderless: - // Preserve borderless flag from the original - renderingFlagForThumbnail = .borderless - case .default, .voiceMessage, .shouldLoop: - // Other cases become default for the still image. - renderingFlagForThumbnail = .default - } - - return try Self.validateContents( - unencryptedFileUrl: tmpFile, - reservedFileIds: reservedFileIds, - mimeType: "image/jpeg", - renderingFlag: renderingFlagForThumbnail, - sourceFilename: sourceFilename - ) - } - - // MARK: Content Type Validation - - static let supportedVideoMimeTypes: Set = [ - "video/3gpp", - "video/3gpp2", - "video/mp4", - "video/quicktime", - "video/x-m4v", - "video/mpeg", - ] - static let supportedAudioMimeTypes: Set = [ - "audio/aac", - "audio/x-m4p", - "audio/x-m4b", - "audio/x-m4a", - "audio/wav", - "audio/x-wav", - "audio/x-mpeg", - "audio/mpeg", - "audio/mp4", - "audio/mp3", - "audio/mpeg3", - "audio/x-mp3", - "audio/x-mpeg3", - "audio/aiff", - "audio/x-aiff", - "audio/3gpp2", - "audio/3gpp", - ] - static let supportedImageMimeTypes: Set = [ - "image/jpeg", - "image/pjpeg", - "image/png", - "image/tiff", - "image/x-tiff", - "image/bmp", - "image/x-windows-bmp", - "image/heic", - "image/heif", - "image/webp", - ] - - static let supportedDefinitelyAnimatedMimeTypes: Set = [ - "image/gif", - "image/apng", - "image/vnd.mozilla.apng", - ] - - public static let supportedMaybeAnimatedMimeTypes: Set = Set([ - "image/webp", - "image/png", - ]).union(supportedDefinitelyAnimatedMimeTypes) - - static func rawContentType(mimeType: String) -> TSAttachmentMigration.V2Attachment.ContentType { - if Self.supportedVideoMimeTypes.contains(mimeType) { - return .video - } else if Self.supportedAudioMimeTypes.contains(mimeType) { - return .audio - } else if Self.supportedDefinitelyAnimatedMimeTypes.contains(mimeType) { - return .animatedImage - } else if Self.supportedImageMimeTypes.contains(mimeType) { - return .image - } else if Self.supportedMaybeAnimatedMimeTypes.contains(mimeType) { - return .animatedImage - } else { - return .file - } - } - - fileprivate struct PendingFile { - let tmpFileUrl: URL - let isTmpFileEncrypted: Bool - let reservedRelativeFilePath: String - - init( - tmpFileUrl: URL, - isTmpFileEncrypted: Bool, - reservedRelativeFilePath: String - ) { - self.tmpFileUrl = tmpFileUrl - self.isTmpFileEncrypted = isTmpFileEncrypted - self.reservedRelativeFilePath = reservedRelativeFilePath - } - } - - private struct ContentTypeResult { - let contentType: TSAttachmentMigration.V2Attachment.ContentType - let audioDurationSeconds: Double? - let mediaSizePixels: CGSize? - let videoDurationSeconds: Double? - let blurHash: String? - let audioWaveformFile: TSAttachmentMigration.V2AttachmentContentValidator.PendingFile? - let videoStillFrameFile: TSAttachmentMigration.V2AttachmentContentValidator.PendingFile? - } - - private static func validateContentType( - unencryptedFileUrl: URL, - byteSize: Int, - reservedFileIds: ReservedRelativeFileIds, - attachmentKey: AttachmentKey, - mimeType: inout String - ) throws -> ContentTypeResult { - let invalidResult = ContentTypeResult( - contentType: .invalid, - audioDurationSeconds: nil, - mediaSizePixels: nil, - videoDurationSeconds: nil, - blurHash: nil, - audioWaveformFile: nil, - videoStillFrameFile: nil - ) - - switch rawContentType(mimeType: mimeType) { - case .invalid: - return invalidResult - case .file: - return ContentTypeResult( - contentType: .file, - audioDurationSeconds: nil, - mediaSizePixels: nil, - videoDurationSeconds: nil, - blurHash: nil, - audioWaveformFile: nil, - videoStillFrameFile: nil - ) - case .image: - guard byteSize < 8 * 1024 * 1024 /* SignalAttachment.kMaxFileSizeImage */ else { - throw AttachmentTooLargeError() - } - return try validateImageContentType( - unencryptedFileUrl, - mimeType: &mimeType - ) ?? invalidResult - case .animatedImage: - guard byteSize < 25 * 1024 * 1024 /* SignalAttachment.kMaxFileSizeAnimatedImage */ else { - throw AttachmentTooLargeError() - } - return try validateImageContentType( - unencryptedFileUrl, - mimeType: &mimeType - ) ?? invalidResult - case .video: - guard byteSize < 95 * 1000 * 1000 /* SignalAttachment.kMaxFileSizeVideo */ else { - throw AttachmentTooLargeError() - } - return try validateVideoContentType( - unencryptedFileUrl, - reservedFileIds: reservedFileIds, - mimeType: mimeType, - attachmentKey: attachmentKey, - ) ?? invalidResult - case .audio: - guard byteSize < 95 * 1000 * 1000 /* SignalAttachment.kMaxFileSizeAudio */ else { - throw AttachmentTooLargeError() - } - return try validateAudioContentType( - unencryptedFileUrl, - reservedFileIds: reservedFileIds, - mimeType: mimeType, - attachmentKey: attachmentKey, - ) ?? invalidResult - } - } - - // MARK: Image/Animated - - // Includes static and animated image validation. - private static func validateImageContentType( - _ unencryptedFileUrl: URL, - mimeType: inout String - ) throws -> ContentTypeResult? { - let imageSource: TSAttachmentMigration.OWSImageSource = try { - do { - return try TSAttachmentMigration.OWSImageSource(fileUrl: unencryptedFileUrl) - } catch { - var errorString = "\(error)" - errorString = errorString.replacingOccurrences(of: "/Attachments/", with: "/[attachment_dir]/") - errorString = errorString.replacingOccurrences(of: unencryptedFileUrl.lastPathComponent, with: "xxxx") - throw OWSAssertionError("Failed to open file handle image source \(errorString)") - } - }() - - guard let imageMetadata = imageSource.imageMetadata( - mimeTypeForValidation: mimeType - ) else { - return nil - } - - guard imageMetadata.isValid else { - return nil - } - - let pixelSize = imageMetadata.pixelSize - - let blurHash: String? = { - guard let image = UIImage(contentsOfFile: unencryptedFileUrl.path) else { - return nil - } - return try? BlurHash.computeBlurHashSync(for: image) - }() - - let contentType: TSAttachmentMigration.V2Attachment.ContentType - if imageMetadata.isAnimated { - contentType = .animatedImage - } else { - contentType = .image - } - return ContentTypeResult( - contentType: contentType, - audioDurationSeconds: nil, - mediaSizePixels: pixelSize, - videoDurationSeconds: nil, - blurHash: blurHash, - audioWaveformFile: nil, - videoStillFrameFile: nil - ) - } - - // MARK: Video - - public class AttachmentTooLargeError: Error {} - - private static func validateVideoContentType( - _ unencryptedFileUrl: URL, - reservedFileIds: ReservedRelativeFileIds, - mimeType: String, - attachmentKey: AttachmentKey, - ) throws -> ContentTypeResult? { - let asset: AVAsset = { - return AVAsset(url: unencryptedFileUrl) - }() - - guard TSAttachmentMigration.OWSMediaUtils.isValidVideo(asset: asset) else { - return nil - } - - let thumbnailImage = try? TSAttachmentMigration.OWSMediaUtils.thumbnail( - forVideo: asset, - maxSizePixels: .square(AttachmentThumbnailQuality.large.thumbnailDimensionPoints()) - ) - guard let thumbnailImage else { - return nil - } - let stillFrameFile: TSAttachmentMigration.V2AttachmentContentValidator.PendingFile? = try thumbnailImage - // Don't compress; we already size-limited this thumbnail, it already has whatever - // compression applied to the source video, and we want a high fidelity still frame. - .jpegData(compressionQuality: 1) - .map { thumbnailData in - let thumbnailTmpFile = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true) - let (encryptedThumbnail, _) = try Cryptography.encrypt(thumbnailData, attachmentKey: attachmentKey) - try encryptedThumbnail.write(to: thumbnailTmpFile) - return TSAttachmentMigration.V2AttachmentContentValidator.PendingFile( - tmpFileUrl: thumbnailTmpFile, - isTmpFileEncrypted: true, - reservedRelativeFilePath: TSAttachmentMigration.V2Attachment.relativeFilePath( - reservedUUID: reservedFileIds.videoStillFrame - ) - ) - } - - let blurHash = try? BlurHash.computeBlurHashSync(for: thumbnailImage) - - let duration = asset.duration.seconds - - // We have historically used the size of the still frame as the video size. - let pixelSize = thumbnailImage.pixelSize - - return ContentTypeResult( - contentType: .video, - audioDurationSeconds: nil, - mediaSizePixels: pixelSize, - videoDurationSeconds: duration, - blurHash: blurHash, - audioWaveformFile: nil, - videoStillFrameFile: stillFrameFile - ) - } - - // MARK: Audio - - private static func validateAudioContentType( - _ unencryptedFileUrl: URL, - reservedFileIds: ReservedRelativeFileIds, - mimeType: String, - attachmentKey: AttachmentKey, - ) throws -> ContentTypeResult? { - let duration = try computeAudioDuration(unencryptedFileUrl, mimeType: mimeType) - guard let duration else { - Logger.error("Unable to compute duration, treating audio as invalid file") - return nil - } - - // Don't require the waveform file. - let waveformFile = try? self.createAudioWaveform( - unencryptedFileUrl, - reservedFileIds: reservedFileIds, - mimeType: mimeType, - attachmentKey: attachmentKey, - ) - - return ContentTypeResult( - contentType: .audio, - audioDurationSeconds: duration, - mediaSizePixels: nil, - videoDurationSeconds: nil, - blurHash: nil, - audioWaveformFile: waveformFile, - videoStillFrameFile: nil - ) - } - - private static func computeAudioDuration(_ unencryptedFileUrl: URL, mimeType: String) throws -> TimeInterval? { - do { - let player = try AVAudioPlayer(contentsOf: unencryptedFileUrl) - player.prepareToPlay() - return player.duration - } catch { - let pathExtension = unencryptedFileUrl.pathExtension - if - pathExtension == "aac" - || mimeType == "audio/aac" - || mimeType == "audio/x-aac" - { - // AVAudioPlayer can't handle aac file extensions, but _should_ work - // if we just change the extension. - Logger.info("Failed aac file, retrying as m4a") - let newTmpURL = OWSFileSystem.temporaryFileUrl( - fileExtension: "m4a", - isAvailableWhileDeviceLocked: true - ) - do { - try FileManager.default.copyItem(at: unencryptedFileUrl, to: newTmpURL) - } catch { - throw OWSAssertionError("Failed to copy attachment file") - } - let player: AVAudioPlayer - do { - player = try AVAudioPlayer(contentsOf: newTmpURL) - } catch { - Logger.error("Failed to read aac file after reapplying extension") - return nil - } - player.prepareToPlay() - let duration = player.duration - try FileManager.default.removeItem(at: newTmpURL) - return duration - } - Logger.error("Failed reading audio file, mimeType: \(mimeType) ext: \(pathExtension)") - return nil - } - } - - private enum AudioWaveformFile { - case unencrypted(URL) - case encrypted(URL, encryptionKey: Data) - } - - private static func createAudioWaveform( - _ unencryptedFileUrl: URL, - reservedFileIds: ReservedRelativeFileIds, - mimeType: String, - attachmentKey: AttachmentKey, - ) throws -> TSAttachmentMigration.V2AttachmentContentValidator.PendingFile { - let waveform: TSAttachmentMigration.AudioWaveform = try TSAttachmentMigration.AudioWaveformManager - .buildAudioWaveForm(unencryptedFilePath: unencryptedFileUrl.path) - - let outputWaveformFile = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true) - - let waveformData = try waveform.archive() - let (encryptedWaveform, _) = try Cryptography.encrypt(waveformData, attachmentKey: attachmentKey) - try encryptedWaveform.write(to: outputWaveformFile, options: .atomicWrite) - - return .init( - tmpFileUrl: outputWaveformFile, - isTmpFileEncrypted: true, - reservedRelativeFilePath: TSAttachmentMigration.V2Attachment.relativeFilePath( - reservedUUID: reservedFileIds.audioWaveform - ) - ) - } - - // MARK: - File Preparation - - private static func prepareAttachmentFiles( - _ unencryptedFileUrl: URL, - reservedFileIds: ReservedRelativeFileIds, - attachmentKey: AttachmentKey, - mimeType: String, - renderingFlag: TSAttachmentMigration.V2RenderingFlag, - sourceFilename: String?, - contentResult: ContentTypeResult - ) throws -> TSAttachmentMigration.PendingV2AttachmentFile { - let primaryFilePlaintextHash = try computePlaintextHash(unencryptedFileUrl: unencryptedFileUrl) - - // First encrypt the files that need encrypting. - let (primaryPendingFile, primaryFileMetadata) = try encryptPrimaryFile( - unencryptedFileUrl: unencryptedFileUrl, - reservedFileIds: reservedFileIds, - attachmentKey: attachmentKey, - ) - let primaryFileDigest = primaryFileMetadata.digest - let primaryPlaintextLength = UInt32(exactly: primaryFileMetadata.plaintextLength) - guard let primaryPlaintextLength else { - throw OWSAssertionError("File too large") - } - - let primaryEncryptedLength = UInt32(exactly: try OWSFileSystem.fileSize(of: primaryPendingFile.tmpFileUrl)) - guard let primaryEncryptedLength else { - throw OWSAssertionError("file too large") - } - - let audioWaveformFile = try contentResult.audioWaveformFile?.encryptFileIfNeeded( - attachmentKey: attachmentKey, - ) - let videoStillFrameFile = try contentResult.videoStillFrameFile?.encryptFileIfNeeded( - attachmentKey: attachmentKey, - ) - - // Now we can copy files. - for pendingFile in [primaryPendingFile, audioWaveformFile, videoStillFrameFile].compacted() { - let destinationUrl = TSAttachmentMigration.V2Attachment.absoluteAttachmentFileURL( - relativeFilePath: pendingFile.reservedRelativeFilePath - ) - guard OWSFileSystem.ensureDirectoryExists(destinationUrl.deletingLastPathComponent().path) else { - throw OWSAssertionError("Unable to create directory") - } - if OWSFileSystem.fileOrFolderExists(url: destinationUrl) { - // If something is at our reserved (random) location, since collisions are absurdly - // unlikely, it must mean we previously created the file at the reserved location - // but were interrupted. Delete what was there and keep going. - try OWSFileSystem.deleteFile(url: destinationUrl) - } - try OWSFileSystem.moveFile( - from: pendingFile.tmpFileUrl, - to: destinationUrl - ) - } - - return TSAttachmentMigration.PendingV2AttachmentFile( - blurHash: contentResult.blurHash, - sha256ContentHash: primaryFilePlaintextHash, - encryptedByteCount: primaryEncryptedLength, - unencryptedByteCount: primaryPlaintextLength, - mimeType: mimeType, - encryptionKey: attachmentKey.combinedKey, - digestSHA256Ciphertext: primaryFileDigest, - localRelativeFilePath: primaryPendingFile.reservedRelativeFilePath, - renderingFlag: renderingFlag, - sourceFilename: sourceFilename, - validatedContentType: contentResult.contentType, - audioDurationSeconds: contentResult.audioDurationSeconds, - mediaSizePixels: contentResult.mediaSizePixels, - videoDurationSeconds: contentResult.videoDurationSeconds, - audioWaveformRelativeFilePath: contentResult.audioWaveformFile?.reservedRelativeFilePath, - videoStillFrameRelativeFilePath: contentResult.videoStillFrameFile?.reservedRelativeFilePath - ) - } - - // MARK: - Encryption - - private static func computePlaintextHash(unencryptedFileUrl: URL) throws -> Data { - return try Cryptography.computeSHA256DigestOfFile(at: unencryptedFileUrl) - } - - private static func encryptPrimaryFile( - unencryptedFileUrl: URL, - reservedFileIds: ReservedRelativeFileIds, - attachmentKey: AttachmentKey, - ) throws -> (TSAttachmentMigration.V2AttachmentContentValidator.PendingFile, EncryptionMetadata) { - let outputFile = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true) - let encryptionMetadata = try Cryptography.encryptAttachment( - at: unencryptedFileUrl, - output: outputFile, - attachmentKey: attachmentKey, - ) - return ( - TSAttachmentMigration.V2AttachmentContentValidator.PendingFile( - tmpFileUrl: outputFile, - isTmpFileEncrypted: true, - reservedRelativeFilePath: TSAttachmentMigration.V2Attachment.relativeFilePath( - reservedUUID: reservedFileIds.primaryFile - ) - ), - encryptionMetadata - ) - } - } -} - -extension TSAttachmentMigration.V2AttachmentContentValidator.PendingFile { - - fileprivate func encryptFileIfNeeded( - attachmentKey: AttachmentKey, - ) throws -> Self { - if isTmpFileEncrypted { - return self - } - - let outputFile = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true) - // Encrypt _without_ custom padding; we never send these files - // and just use them locally, so no need for custom padding - // that later requires out-of-band plaintext length tracking - // so we can trim the custom padding at read time. - _ = try Cryptography.encryptFile( - at: tmpFileUrl, - output: outputFile, - attachmentKey: attachmentKey, - ) - return Self( - tmpFileUrl: outputFile, - isTmpFileEncrypted: true, - // Preserve the reserved file path; this is already - // on the ContentType enum and musn't be changed. - reservedRelativeFilePath: self.reservedRelativeFilePath - ) - } -} diff --git a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+AudioWaveformManager.swift b/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+AudioWaveformManager.swift deleted file mode 100644 index a48f05ddf7..0000000000 --- a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+AudioWaveformManager.swift +++ /dev/null @@ -1,320 +0,0 @@ -// -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -import Accelerate -import AVFoundation -import Foundation - -extension TSAttachmentMigration { - - struct AudioWaveform { - let decibelSamples: [Float] - - func archive() throws -> Data { - return try NSKeyedArchiver.archivedData(withRootObject: decibelSamples, requiringSecureCoding: false) - } - } - - class AudioWaveformManager { - - static func buildAudioWaveForm( - unencryptedFilePath: String - ) throws -> TSAttachmentMigration.AudioWaveform { - let asset: AVAsset = try assetFromUnencryptedAudioFile(atAudioPath: unencryptedFilePath) - - guard asset.isReadable else { - throw OWSAssertionError("unexpectedly encountered unreadable audio file.") - } - - guard CMTimeGetSeconds(asset.duration) <= Self.maximumDuration else { - throw OWSAssertionError("Audio too long") - } - - return try sampleWaveform(asset: asset, filePath: unencryptedFilePath) - } - - private static func assetFromUnencryptedAudioFile( - atAudioPath audioPath: String - ) throws -> AVAsset { - let audioUrl = URL(fileURLWithPath: audioPath) - - var asset = AVURLAsset(url: audioUrl) - - if !asset.isReadable { - if let extensionOverride = Self.alternativeAudioFileExtension(fileExtension: audioUrl.pathExtension) { - let symlinkPath = OWSFileSystem.temporaryFilePath( - fileExtension: extensionOverride, - isAvailableWhileDeviceLocked: true - ) - do { - try FileManager.default.createSymbolicLink( - atPath: symlinkPath, - withDestinationPath: audioPath - ) - } catch { - throw OWSAssertionError("Failed to create symlink") - } - asset = AVURLAsset(url: URL(fileURLWithPath: symlinkPath)) - } - } - - return asset - } - - private static func alternativeAudioFileExtension(fileExtension: String) -> String? { - // In some cases, Android sends audio messages with the "audio/mpeg" mime type. This - // makes our choice of file extension ambiguous—`.mp3` or `.m4a`? AVFoundation uses the - // extension to read the file, and if the extension is wrong, it won't be readable. - // - // We "lie" about the extension to generate the waveform so that AVFoundation may read - // it. This is brittle but necessary to work around the buggy marriage of Android's - // content type and AVFoundation's behavior. - // - // Note that we probably still want this code even if Android updates theirs, because - // iOS users might have existing attachments. - // - // See: - // . - switch fileExtension { - case "m4a": return "aac" - case "mp3": return "m4a" - default: return nil - } - } - - // MARK: - Sampling - - /// The maximum duration asset that we will display waveforms for. - /// It's too intensive to sample a waveform for really long audio files. - private static let maximumDuration: TimeInterval = 15 * .minute - private static let sampleCount = 100 - - private static func sampleWaveform(asset: AVAsset, filePath: String) throws -> TSAttachmentMigration.AudioWaveform { - let assetReader = try AVAssetReader(asset: asset) - - // We just draw the waveform based on the first track. - guard let audioTrack = assetReader.asset.tracks.first else { - throw OWSAssertionError("audio file has no tracks") - } - - let lastAttemptedFilePathKey = "TSAttachmentMigrationLastAudioWaveformAttempt" - let lastAttemptedFilePath = UserDefaults.standard.string(forKey: lastAttemptedFilePathKey) - if lastAttemptedFilePath == filePath { - // Previously tried to open an AVAssetReaderTrackOutput but crashed. - // Throw an error so we skip this audio file; treat it as corrupted. - throw OWSAssertionError("Unable to generate AVAssetReaderTrackOutput on previous run") - } - UserDefaults.standard.setValue(filePath, forKey: lastAttemptedFilePathKey) - - let trackOutput = AVAssetReaderTrackOutputWrapper.safeAssetReaderTrackOutput( - with: audioTrack, - outputSettings: [ - AVFormatIDKey: kAudioFormatLinearPCM, - AVLinearPCMBitDepthKey: 16, - AVLinearPCMIsBigEndianKey: false, - AVLinearPCMIsFloatKey: false, - AVLinearPCMIsNonInterleaved: false - ] - ) - UserDefaults.standard.removeObject(forKey: lastAttemptedFilePathKey) - - guard let trackOutput else { - throw OWSAssertionError("unable to generate audio track") - } - assetReader.add(trackOutput) - - let decibelSamples = try readDecibels(from: assetReader) - - return TSAttachmentMigration.AudioWaveform(decibelSamples: decibelSamples) - } - - private static func readDecibels(from assetReader: AVAssetReader) throws -> [Float] { - let sampler = AudioWaveformSampler( - inputCount: sampleCount(from: assetReader), - outputCount: Self.sampleCount - ) - - assetReader.startReading() - while assetReader.status == .reading { - guard let trackOutput = assetReader.outputs.first else { - throw OWSAssertionError("track output unexpectedly missing") - } - - // Process any newly read data. - guard - let nextSampleBuffer = trackOutput.copyNextSampleBuffer(), - let blockBuffer = CMSampleBufferGetDataBuffer(nextSampleBuffer) - else { - // There is no more data to read, break - break - } - - var lengthAtOffset = 0 - var dataPointer: UnsafeMutablePointer? - let result = CMBlockBufferGetDataPointer( - blockBuffer, - atOffset: 0, - lengthAtOffsetOut: &lengthAtOffset, - totalLengthOut: nil, - dataPointerOut: &dataPointer - ) - guard result == kCMBlockBufferNoErr else { - owsFailDebug("track data unexpectedly inaccessible") - throw AudioWaveformError.invalidAudioFile - } - let bufferPointer = UnsafeBufferPointer(start: dataPointer, count: lengthAtOffset) - bufferPointer.withMemoryRebound(to: Int16.self) { sampler.update($0) } - CMSampleBufferInvalidate(nextSampleBuffer) - } - - return sampler.finalize() - } - - private static func sampleCount(from assetReader: AVAssetReader) -> Int { - let samplesPerChannel = Int(assetReader.asset.duration.value) - - // We will read in the samples from each channel, interleaved since - // we only draw one waveform. This gives us an average of the channels - // if it is, for example, a stereo audio file. - return samplesPerChannel * channelCount(from: assetReader) - } - - private static func channelCount(from assetReader: AVAssetReader) -> Int { - guard - let output = assetReader.outputs.first as? AVAssetReaderTrackOutput, - let formatDescriptions = output.track.formatDescriptions as? [CMFormatDescription] - else { - return 0 - } - - var channelCount = 0 - - for description in formatDescriptions { - guard let basicDescription = CMAudioFormatDescriptionGetStreamBasicDescription(description) else { - continue - } - channelCount = Int(basicDescription.pointee.mChannelsPerFrame) - } - - return channelCount - } - } - - private class AudioWaveformSampler { - private static let silenceThreshold: Float = -50 - - private let inputCount: Int - private let outputCount: Int - - /// The number of input samples that feed each output sample (rounded down). - private let segmentLength: Int - - /// The number of samples that don't evenly divide into `outputCount`. These - /// extra samples are spread across the output samples. - private let segmentRemainder: Int - - /// The number of samples in this segment. Either `segmentLength` or - /// `segmentLength + 1`. - private var currentSegmentCount: Int - - /// The number of samples remaining in this segment. - private var currentSegmentRemainingCount: Int - - /// Tracks the cumulative average when a segment spans multiple batches. - private var currentSegmentAverage: Float - - /// Tracks when a segment needs an extra sample (because outputCount may not - /// evenly divide inputCount). - private var overflowCounter: Int - - private var buffer = [Float]() - private var output = [Float]() - - init(inputCount: Int, outputCount: Int) { - self.inputCount = inputCount - self.outputCount = outputCount - if inputCount < outputCount { - // If we don't have enough samples, just use every sample that's provided. - // This will result in fewer than outputCount samples, but that is fine. - (self.segmentLength, self.segmentRemainder) = (1, 0) - } else { - (self.segmentLength, self.segmentRemainder) = inputCount.quotientAndRemainder(dividingBy: outputCount) - } - self.currentSegmentAverage = 0 - // The first segment is always segmentLength because segmentRemainder is - // less than outputCount (it's the remainder when dividing by outputCount). - self.currentSegmentCount = self.segmentLength - self.currentSegmentRemainingCount = self.segmentLength - self.overflowCounter = self.outputCount - self.segmentRemainder - } - - func update(_ samples: UnsafeBufferPointer) { - let sampleCount = samples.count - if self.buffer.count < sampleCount { - self.buffer.append(contentsOf: Array(repeating: 0, count: sampleCount - self.buffer.count)) - } - - // convert UInt16 amplitudes to Float representation - vDSP_vflt16(samples.baseAddress!, 1, &self.buffer, 1, vDSP_Length(sampleCount)) - - // take the absolute amplitude value - vDSP_vabs(self.buffer, 1, &self.buffer, 1, vDSP_Length(sampleCount)) - - // convert to dB - // maximum amplitude storable in Int16 = 0 dB (loudest) - // (remember decibels are often negative) - var zeroDecibelEquivalent: Float = Float(Int16.max) - vDSP_vdbcon(self.buffer, 1, &zeroDecibelEquivalent, &self.buffer, 1, vDSP_Length(sampleCount), 1) - - // clip between loudest + quietest - var loudestClipValue: Float = 0.0 - var quietestClipValue = AudioWaveformSampler.silenceThreshold - vDSP_vclip(self.buffer, 1, &quietestClipValue, &loudestClipValue, &self.buffer, 1, vDSP_Length(sampleCount)) - - self.reduce(sampleCount: sampleCount) - } - - private func reduce(sampleCount: Int) { - self.buffer.withUnsafeBufferPointer { bufferPtr in - var remainingCount = sampleCount - while remainingCount > 0 { - let chunkCount = min(remainingCount, self.currentSegmentRemainingCount) - assert(chunkCount > 0) // because currentSegmentRemainingCount starts > 0 and is checked on each iteration - var chunkAverage: Float = 0 - vDSP_meanv(bufferPtr.baseAddress!.advanced(by: sampleCount - remainingCount), 1, &chunkAverage, vDSP_Length(chunkCount)) - remainingCount -= chunkCount - self.currentSegmentRemainingCount -= chunkCount - - // Add the new average to the running average for this segment. - let totalChunkCount = self.currentSegmentCount - self.currentSegmentRemainingCount - assert(totalChunkCount > 0) // because chunkCount > 0 - let newChunkWeight = Float(chunkCount) / Float(totalChunkCount) - let oldChunkWeight = 1 - newChunkWeight - self.currentSegmentAverage *= oldChunkWeight - self.currentSegmentAverage += chunkAverage * newChunkWeight - - // If we reached the end of the chunk, add it to the output. - if self.currentSegmentRemainingCount <= 0 { - self.output.append(self.currentSegmentAverage) - self.currentSegmentAverage = 0 // technically redundant - - self.currentSegmentCount = self.segmentLength - self.overflowCounter -= self.segmentRemainder - if self.overflowCounter <= 0 { - self.currentSegmentCount += 1 - self.overflowCounter += self.segmentLength - } - self.currentSegmentRemainingCount = self.currentSegmentCount - } - } - } - } - - func finalize() -> [Float] { - assert(self.output.count <= self.outputCount) - return self.output - } - } -} diff --git a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+ImageMetadata.swift b/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+ImageMetadata.swift deleted file mode 100644 index 896e5d79c0..0000000000 --- a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+ImageMetadata.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -import Foundation - -extension TSAttachmentMigration { - enum ImageFormat { - case unknown - case png - case gif - case tiff - case jpeg - case bmp - case webp - case heic - case heif - - var mimeType: String? { - switch self { - case .png: - return "image/png" - case .gif: - return "image/gif" - case .tiff: - return "image/tiff" - case .jpeg: - return "image/jpeg" - case .bmp: - return "image/bmp" - case .webp: - return "image/webp" - case .heic: - return "image/heic" - case .heif: - return "image/heif" - case .unknown: - return nil - } - } - - func isValid(source: TSAttachmentMigration.OWSImageSource) -> Bool { - switch self { - case .unknown: - return false - case .png, .gif, .tiff, .jpeg, .bmp, .webp, .heic, .heif: - return true - } - } - - func isValid(mimeType: String?) -> Bool { - owsAssertDebug(!(mimeType?.isEmpty ?? true)) - - switch self { - case .unknown: - return false - case .png: - guard let mimeType else { return true } - return (mimeType.caseInsensitiveCompare("image/png") == .orderedSame || - mimeType.caseInsensitiveCompare("image/apng") == .orderedSame || - mimeType.caseInsensitiveCompare("image/vnd.mozilla.apng") == .orderedSame) - case .gif: - guard let mimeType else { return true } - return mimeType.caseInsensitiveCompare("image/gif") == .orderedSame - case .tiff: - guard let mimeType else { return true } - return (mimeType.caseInsensitiveCompare("image/tiff") == .orderedSame || - mimeType.caseInsensitiveCompare("image/x-tiff") == .orderedSame) - case .jpeg: - guard let mimeType else { return true } - return mimeType.caseInsensitiveCompare("image/jpeg") == .orderedSame - case .bmp: - guard let mimeType else { return true } - return (mimeType.caseInsensitiveCompare("image/bmp") == .orderedSame || - mimeType.caseInsensitiveCompare("image/x-windows-bmp") == .orderedSame) - case .webp: - guard let mimeType else { return true } - return mimeType.caseInsensitiveCompare("image/webp") == .orderedSame - case .heic: - guard let mimeType else { return true } - return mimeType.caseInsensitiveCompare("image/heic") == .orderedSame - case .heif: - guard let mimeType else { return true } - return mimeType.caseInsensitiveCompare("image/heif") == .orderedSame - } - } - } - - struct ImageMetadata { - let isValid: Bool - let imageFormat: ImageFormat - let pixelSize: CGSize - let hasAlpha: Bool - let isAnimated: Bool - - static func invalid() -> Self { - .init(isValid: false, imageFormat: .unknown, pixelSize: .zero, hasAlpha: false, isAnimated: false) - } - - var mimeType: String? { - imageFormat.mimeType - } - - var fileExtension: String? { - guard let mimeType else { - return nil - } - return MimeTypeUtil.fileExtensionForMimeType(mimeType) - } - } - - internal struct WebpMetadata { - let isValid: Bool - let canvasWidth: UInt32 - let canvasHeight: UInt32 - let frameCount: UInt32 - } -} diff --git a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+OWSImageSource.swift b/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+OWSImageSource.swift deleted file mode 100644 index 4418ba7cd0..0000000000 --- a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+OWSImageSource.swift +++ /dev/null @@ -1,564 +0,0 @@ -// -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -import Foundation -import libwebp - -// MARK: - PNG Chunker - -extension TSAttachmentMigration { - /// Helps you iterate over PNG chunks from raw data. - /// - /// let chunker = try PngChunker(data: myPngData) - /// while let chunk = try chunker.next() { - /// let type = String(data: chunk.type, encoding: .ascii)! - /// print("Found a chunk of type \(type)") - /// } - /// - /// Useful for low-level handling of PNGs, not image processing. - /// - /// Quick background on PNGs: PNG files always start with the same 8 - /// bytes (the "PNG signature") and then contain several chunks. Chunks - /// have a type (like `IHDR` for the image metadata header) and 0 or - /// more bytes of chunk-specific data. Chunks also have two computable - /// fields: the length of the data and a checksum. - /// - /// For more, see the ["Chunk layout" section][0] of the PNG spec. - /// - /// [0]: https://www.w3.org/TR/2003/REC-PNG-20031110/#5Chunk-layout - fileprivate class PngChunker { - /// The PNG signature, lifted from [the spec][1]. - /// [1]: https://www.w3.org/TR/2003/REC-PNG-20031110/#5PNG-file-signature - fileprivate static let pngSignature = Data([137, 80, 78, 71, 13, 10, 26, 10]) - - /// The smallest possible PNG size is a well-compressed 1x1 image. - /// - /// 8 bytes for the PNG signature, - /// plus 25 bytes for the IHDR chunk (12 for metadata + 13 for data), - /// plus 22 bytes for the IDAT chunk (12 for metadata + 10 for 1 black pixel), - /// plus 12 bytes for the IEND chunk (12 for metadata, no data), - /// = 67 bytes. - private static let smallestPossiblePngSize = 67 - - private let pngSource: TSAttachmentMigration.OWSImageSource - - /// The current index we're looking at. If nil, we're done looking at the data. - private var cursor: Int? - - /// Initialize a PNG chunker. - /// - Parameter imageSource: Source for a PNG. - fileprivate init(source: TSAttachmentMigration.OWSImageSource) throws { - guard source.byteLength >= Self.smallestPossiblePngSize else { - throw OWSAssertionError("png too small") - } - let prefix = try? source.readData(byteOffset: 0, byteLength: Self.pngSignature.count) - guard prefix == Self.pngSignature else { - throw OWSAssertionError("File does not start with png signature") - } - pngSource = source - cursor = Self.pngSignature.count - } - - /// Get the next PNG chunk. - /// - Returns: The next chunk, or `nil` if the end of the data has been reached. - fileprivate func next() throws -> TSAttachmentMigration.PngChunker.Chunk? { - guard var cursor = cursor, cursor < pngSource.byteLength else { - return nil - } - - // Checks that there's enough space for the length (4 bytes) and the type (4 bytes). - guard cursor + 8 <= pngSource.byteLength else { - self.cursor = nil - throw OWSAssertionError("Ended unexpectedly") - } - - let lengthBytes = try pngSource.readData(byteOffset: cursor, byteLength: 4) - let length = try Self.asPngUInt32(lengthBytes) - cursor += 4 - - var expectedCrc = CRC32() - - let type = try pngSource.readData(byteOffset: cursor, byteLength: 4) - guard Self.isValidPngType(type) else { - self.cursor = nil - throw OWSAssertionError("Invalid chunk type") - } - expectedCrc = expectedCrc.update(with: type) - cursor += 4 - - // Checks that there's enough space for the data (N bytes) and the CRC (4 bytes). - let lengthAsInt = Int(length) - guard cursor + lengthAsInt + 4 <= pngSource.byteLength else { - self.cursor = nil - throw OWSAssertionError("Ended unexpectedly") - } - let data = try pngSource.readData(byteOffset: cursor, byteLength: lengthAsInt) - expectedCrc = expectedCrc.update(with: data) - cursor += lengthAsInt - - let crcBytes = try pngSource.readData(byteOffset: cursor, byteLength: 4) - let actualCrc = try Self.asPngUInt32(crcBytes) - cursor += 4 - - guard actualCrc == expectedCrc.value else { - self.cursor = nil - throw OWSAssertionError("Invalid checksum") - } - - self.cursor = cursor - - return Chunk( - lengthBytes: lengthBytes, - type: type, - data: data, - crcBytes: crcBytes - ) - } - - // MARK: - Chunk - - /// A single PNG chunk. Holds the length, type, data, and CRC checksum. - /// - /// For details, see the ["Chunk layout" section][0] of the PNG spec. - /// - /// [0]: https://www.w3.org/TR/2003/REC-PNG-20031110/#5Chunk-layout - fileprivate struct Chunk { - /// The chunk data's length, encoded as a PNG 32-bit big endian number. - let lengthBytes: Data - - /// The chunk's type, as raw data. - /// - /// You may wish to convert this to a string. This is just a normal ASCII conversion: - /// - /// let typeString = String(data: myChunk.type, encoding: .ascii) - let type: Data - - /// The chunk's data. - let data: Data - - /// The chunk's CRC32 code, encoded as a PNG 32-bit big endian number. - let crcBytes: Data - - /// Get all the bytes for this chunk. - /// - /// Includes all four sections: the length, type, data, and checksum. - /// - /// - Returns: The full chunk in bytes. - func allBytes() -> Data { - lengthBytes + type + data + crcBytes - } - } - } -} - -extension TSAttachmentMigration.PngChunker { - static func asPngUInt32(_ data: Data) throws -> UInt32 { - var result: UInt32 = 0 - for (i, byte) in data.reversed().enumerated() { - result += UInt32(byte) * (1 << (8 * i)) - } - return result - } - - static func isValidPngType(_ data: Data) -> Bool { - guard data.count == 4 else { return false } - func isAsciiLetter(_ byte: UInt8) -> Bool { - (65...90).contains(byte) || (97...122).contains(byte) - } - return data.allSatisfy(isAsciiLetter) - } -} - -// MARK: - Image Validator - -extension TSAttachmentMigration { - - struct OWSImageSource { - - let fileHandle: FileHandle - let byteLength: Int - - init(fileUrl: URL) throws { - self.byteLength = Int((try? OWSFileSystem.fileSize(of: fileUrl)) ?? 0) - self.fileHandle = try FileHandle(forReadingFrom: fileUrl) - } - - func readData(byteOffset: Int, byteLength: Int) throws -> Data { - if try fileHandle.offset() != byteOffset { - fileHandle.seek(toFileOffset: UInt64(byteOffset)) - } - return try fileHandle.read(upToCount: byteLength) ?? Data() - } - - func readIntoMemory() throws -> Data { - if try fileHandle.offset() != 0 { - fileHandle.seek(toFileOffset: 0) - } - return try fileHandle.readToEnd() ?? Data() - } - - // Class-bound wrapper around FileHandle - class FileHandleWrapper { - let fileHandle: FileHandle - - init(_ fileHandle: FileHandle) { - self.fileHandle = fileHandle - } - } - - func cgImageSource() throws -> CGImageSource? { - let fileHandle = FileHandleWrapper(fileHandle) - - var callbacks = CGDataProviderDirectCallbacks( - version: 0, - getBytePointer: nil, - releaseBytePointer: nil, - getBytesAtPosition: { info, buffer, offset, byteCount in - guard - let unmanagedFileHandle = info?.assumingMemoryBound( - to: Unmanaged.self - ).pointee - else { - return 0 - } - let fileHandle = unmanagedFileHandle.takeUnretainedValue().fileHandle - do { - if offset != (try fileHandle.offset()) { - try fileHandle.seek(toOffset: UInt64(offset)) - } - let data = try fileHandle.read(upToCount: byteCount) ?? Data() - data.withUnsafeBytes { bytes in - buffer.copyMemory(from: bytes.baseAddress!, byteCount: bytes.count) - } - return data.count - } catch { - return 0 - } - }, - releaseInfo: { info in - guard - let unmanagedFileHandle = info?.assumingMemoryBound( - to: Unmanaged.self - ).pointee - else { - return - } - unmanagedFileHandle.release() - } - ) - - var unmanagedFileHandle = Unmanaged.passRetained(fileHandle) - - guard let dataProvider = CGDataProvider( - directInfo: &unmanagedFileHandle, - size: Int64(byteLength), - callbacks: &callbacks - ) else { - throw OWSAssertionError("Failed to create data provider") - } - return CGImageSourceCreateWithDataProvider(dataProvider, nil) - } - - fileprivate static func ows_isValidImage(dimension imageSize: CGSize, depthBytes: CGFloat, isAnimated: Bool) -> Bool { - if imageSize.width < 1 || imageSize.height < 1 || depthBytes < 1 { - // Invalid metadata. - return false - } - - // We only support (A)RGB and (A)Grayscale, so worst case is 4. - let worstCaseComponentsPerPixel = CGFloat(4) - let bytesPerPixel = worstCaseComponentsPerPixel * depthBytes - - let expectedBytesPerPixel: CGFloat = 4 - let maxValidImageDimension: CGFloat = CGFloat(isAnimated ? TSAttachmentMigration.kMaxAnimatedImageDimensions : TSAttachmentMigration.kMaxStillImageDimensions) - let maxBytes = maxValidImageDimension * maxValidImageDimension * expectedBytesPerPixel - let actualBytes = imageSize.width * imageSize.height * bytesPerPixel - if actualBytes > maxBytes { - Logger.warn("invalid dimensions width: \(imageSize.width), height \(imageSize.height), bytesPerPixel: \(bytesPerPixel)") - return false - } - - return true - } - - fileprivate func ows_guessHighEfficiencyImageFormat() -> ImageFormat { - // A HEIF image file has the first 16 bytes like - // 0000 0018 6674 7970 6865 6963 0000 0000 - // so in this case the 5th to 12th bytes shall make a string of "ftypheic" - let heifHeaderStartsAt = 4 - let heifBrandStartsAt = 8 - // We support "heic", "mif1" or "msf1". Other brands are invalid for us for now. - // The length is 4 + 1 because the brand must be terminated with a null. - // Include the null in the comparison to prevent a bogus brand like "heicfake" - // from being considered valid. - let heifSupportedBrandLength = 5 - let totalHeaderLength = heifBrandStartsAt - heifHeaderStartsAt + heifSupportedBrandLength - guard byteLength >= heifBrandStartsAt + heifSupportedBrandLength else { - return .unknown - } - - // These are the brands of HEIF formatted files that are renderable by CoreGraphics - let heifBrandHeaderHeic = Data("ftypheic\0".utf8) - let heifBrandHeaderHeif = Data("ftypmif1\0".utf8) - let heifBrandHeaderHeifStream = Data("ftypmsf1\0".utf8) - - // Pull the string from the header and compare it with the supported formats - let header = try? readData(byteOffset: heifHeaderStartsAt, byteLength: totalHeaderLength) - - if header == heifBrandHeaderHeic { - return .heic - } else if header == heifBrandHeaderHeif || header == heifBrandHeaderHeifStream { - return .heif - } else { - return .unknown - } - } - - fileprivate func ows_guessImageFormat() -> ImageFormat { - guard byteLength >= 2 else { - return .unknown - } - - switch try? readData(byteOffset: 0, byteLength: 2) { - case Data([0x47, 0x49]): - return .gif - case Data([0x89, 0x50]): - return .png - case Data([0xff, 0xd8]): - return .jpeg - case Data([0x42, 0x4d]): - return .bmp - case Data([0x4d, 0x4d]), // Motorola byte order TIFF - Data([0x49, 0x49]): // Intel byte order TIFF - return .tiff - case Data([0x52, 0x49]): - // First two letters of RIFF tag. - return .webp - default: - return ows_guessHighEfficiencyImageFormat() - } - } - - fileprivate static func applyImageOrientation(_ orientation: CGImagePropertyOrientation, to imageSize: CGSize) -> CGSize { - // NOTE: UIImageOrientation and CGImagePropertyOrientation values - // DO NOT match. - switch orientation { - case .up, .upMirrored, .down, .downMirrored: - return imageSize - case .left, .leftMirrored, .right, .rightMirrored: - return CGSize(width: imageSize.height, height: imageSize.width) - } - } - - /// Determine whether something is an animated PNG. - /// - /// Does this by checking that the `acTL` chunk appears before any `IDAT` chunk. - /// See [the APNG spec][0] for more. - /// - /// [0]: https://wiki.mozilla.org/APNG_Specification - /// - /// - Returns: - /// `true` if the contents appear to be an APNG. - /// `false` if the contents are a still PNG. - /// `nil` if the contents are invalid. - func isAnimatedPngData() -> NSNumber? { - let actl = "acTL".data(using: .ascii) - let idat = "IDAT".data(using: .ascii) - - do { - let chunker = try PngChunker(source: self) - while let chunk = try chunker.next() { - if chunk.type == actl { - return NSNumber(value: true) - } else if chunk.type == idat { - return NSNumber(value: false) - } - } - } catch { - Logger.warn("Error: \(error)") - } - - return nil - } - - // MARK: - Image Metadata - - func imageMetadata( - mimeTypeForValidation declaredMimeType: String? - ) -> TSAttachmentMigration.ImageMetadata? { - guard byteLength < TSAttachmentMigration.kMaxFileSizeGeneric else { - return nil - } - - let imageFormat = ows_guessImageFormat() - guard imageFormat.isValid(source: self) else { - Logger.warn("Image does not have valid format.") - return nil - } - - guard imageFormat.mimeType != nil else { - Logger.warn("Image does not have MIME type.") - return nil - } - - let isAnimated: Bool - switch imageFormat { - case .gif: - // This treats all GIFs as animated. We could reflect the actual image content. - isAnimated = true - case .webp: - let webpMetadata = metadataForWebp - guard webpMetadata.isValid else { - Logger.warn("Image does not have valid webpMetadata.") - return nil - } - isAnimated = webpMetadata.frameCount > 1 - case .png: - guard let isAnimatedPng = isAnimatedPngData() else { - Logger.warn("Could not determine if png is animated.") - return nil - } - isAnimated = isAnimatedPng.boolValue - default: - isAnimated = false - } - - guard imageFormat.isValid(source: self) else { - Logger.warn("Image does not have valid format.") - return nil - } - - if isAnimated, byteLength > TSAttachmentMigration.kMaxFileSizeAnimatedImage { - Logger.warn("Oversize image.") - return nil - } else if !isAnimated, byteLength > TSAttachmentMigration.kMaxFileSizeImage { - Logger.warn("Oversize image.") - return nil - } - - let metadata = imageMetadata(withIsAnimated: isAnimated, imageFormat: imageFormat) - - guard metadata.isValid else { - return nil - } - - return metadata - } - - fileprivate func imageMetadata( - withIsAnimated isAnimated: Bool, - imageFormat: TSAttachmentMigration.ImageFormat - ) -> TSAttachmentMigration.ImageMetadata { - if imageFormat == .webp { - let imageSize = sizeForWebpData - guard Self.ows_isValidImage(dimension: imageSize, depthBytes: 1, isAnimated: isAnimated) else { - Logger.warn("Image does not have valid dimensions: \(imageSize)") - return .invalid() - } - return .init(isValid: true, imageFormat: imageFormat, pixelSize: imageSize, hasAlpha: true, isAnimated: isAnimated) - } - - guard let imageSource = try? self.cgImageSource() else { - Logger.warn("Could not build imageSource.") - return .invalid() - } - return Self.imageMetadata(withImageSource: imageSource, imageFormat: imageFormat, isAnimated: isAnimated) - } - - fileprivate static func imageMetadata( - withImageSource imageSource: CGImageSource, - imageFormat: TSAttachmentMigration.ImageFormat, - isAnimated: Bool - ) -> TSAttachmentMigration.ImageMetadata { - let options = [kCGImageSourceShouldCache as String: false] - guard let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, options as CFDictionary) as? [String: AnyObject] else { - Logger.warn("Missing imageProperties.") - return .invalid() - } - - guard let widthNumber = imageProperties[kCGImagePropertyPixelWidth as String] as? NSNumber else { - Logger.warn("widthNumber was unexpectedly nil") - return .invalid() - } - guard let heightNumber = imageProperties[kCGImagePropertyPixelHeight as String] as? NSNumber else { - Logger.warn("heightNumber was unexpectedly nil") - return .invalid() - } - - var pixelSize = CGSize(width: widthNumber.doubleValue, height: heightNumber.doubleValue) - if let orientationNumber = imageProperties[kCGImagePropertyOrientation as String] as? NSNumber { - guard let orientation = CGImagePropertyOrientation(rawValue: orientationNumber.uint32Value) else { - Logger.warn("orientation number was invalid") - return .invalid() - } - pixelSize = applyImageOrientation(orientation, to: pixelSize) - } - - let hasAlpha = imageProperties[kCGImagePropertyHasAlpha as String] as? NSNumber ?? false - - // The number of bits in each color sample of each pixel. The value of this key is a CFNumberRef. - guard let depthNumber = imageProperties[kCGImagePropertyDepth as String] as? NSNumber else { - Logger.warn("depthNumber was unexpectedly nil") - return .invalid() - } - let depthBits = depthNumber.uintValue - // This should usually be 1. - let depthBytes = ceil(Double(depthBits) / 8.0) - - // The color model of the image such as "RGB", "CMYK", "Gray", or "Lab". The value of this key is CFStringRef. - guard let colorModel = (imageProperties[kCGImagePropertyColorModel as String] as? NSString) as String? else { - Logger.warn("colorModel was unexpectedly nil") - return .invalid() - } - guard colorModel == kCGImagePropertyColorModelRGB as String || colorModel == kCGImagePropertyColorModelGray as String else { - Logger.warn("Invalid colorModel: \(colorModel)") - return .invalid() - } - - guard ows_isValidImage(dimension: pixelSize, depthBytes: depthBytes, isAnimated: isAnimated) else { - Logger.warn("Image does not have valid dimensions: \(pixelSize).") - return .invalid() - } - - return .init(isValid: true, imageFormat: imageFormat, pixelSize: pixelSize, hasAlpha: hasAlpha.boolValue, isAnimated: isAnimated) - } - - // MARK: - WEBP - - fileprivate var sizeForWebpData: CGSize { - let webpMetadata = metadataForWebp - guard webpMetadata.isValid else { - return .zero - } - return .init(width: CGFloat(webpMetadata.canvasWidth), height: CGFloat(webpMetadata.canvasHeight)) - } - - fileprivate var metadataForWebp: TSAttachmentMigration.WebpMetadata { - guard let data = try? self.readIntoMemory() else { - return WebpMetadata(isValid: false, canvasWidth: 0, canvasHeight: 0, frameCount: 0) - } - return data.withUnsafeBytes { - $0.withMemoryRebound(to: UInt8.self) { buffer in - var webPData = WebPData(bytes: buffer.baseAddress, size: buffer.count) - guard let demuxer = WebPDemux(&webPData) else { - return WebpMetadata(isValid: false, canvasWidth: 0, canvasHeight: 0, frameCount: 0) - } - defer { - WebPDemuxDelete(demuxer) - } - - let canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH) - let canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT) - let frameCount = WebPDemuxGetI(demuxer, WEBP_FF_FRAME_COUNT) - let result = WebpMetadata(isValid: canvasWidth > 0 && canvasHeight > 0 && frameCount > 0, - canvasWidth: canvasWidth, - canvasHeight: canvasHeight, - frameCount: frameCount) - return result - } - } - } - } -} diff --git a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+OWSMediaUtils.swift b/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+OWSMediaUtils.swift deleted file mode 100644 index d3c4e7a550..0000000000 --- a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+OWSMediaUtils.swift +++ /dev/null @@ -1,217 +0,0 @@ -// -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -import AVKit -import Foundation - -extension TSAttachmentMigration { - - static let kMaxAnimatedImageDimensions: UInt = 12 * 1024 - static let kMaxStillImageDimensions: UInt = 12 * 1024 - static let kMaxFileSizeAnimatedImage = UInt(25 * 1024 * 1024) - static let kMaxFileSizeImage = UInt(8 * 1024 * 1024) - static let kMaxFileSizeGeneric = UInt(95 * 1000 * 1000) - - enum OWSMediaUtils { - // This size is large enough to render full screen. - static func thumbnailDimensionPointsLarge() -> CGFloat { - let screenSizePoints = UIScreen.main.bounds.size - return max(screenSizePoints.width, screenSizePoints.height) - } - - static func isValidVideo(asset: AVAsset) -> Bool { - var maxTrackSize = CGSize.zero - for track: AVAssetTrack in asset.tracks(withMediaType: .video) { - let trackSize: CGSize = track.naturalSize - maxTrackSize.width = max(maxTrackSize.width, trackSize.width) - maxTrackSize.height = max(maxTrackSize.height, trackSize.height) - } - if maxTrackSize.width < 1.0 || maxTrackSize.height < 1.0 { - Logger.error("Invalid video size: \(maxTrackSize)") - return false - } - if maxTrackSize.width > 4096 || maxTrackSize.height > 4096 { - Logger.error("Invalid video dimensions: \(maxTrackSize)") - return false - } - return true - } - - static func thumbnail(forImage image: UIImage, maxDimensionPixels: CGFloat) throws -> UIImage { - if image.pixelSize.width <= maxDimensionPixels, - image.pixelSize.height <= maxDimensionPixels { - let result = image.withNativeScale - return result - } - guard let thumbnailImage = Self.resize(image: image, maxDimensionPixels: maxDimensionPixels) else { - throw OWSAssertionError("Could not thumbnail image.") - } - guard nil != thumbnailImage.cgImage else { - throw OWSAssertionError("Missing cgImage.") - } - let result = thumbnailImage.withNativeScale - return result - } - - static func thumbnail(forVideo asset: AVAsset, maxSizePixels: CGSize) throws -> UIImage { - let generator = AVAssetImageGenerator(asset: asset) - generator.maximumSize = maxSizePixels - generator.appliesPreferredTrackTransform = true - let time: CMTime = CMTimeMake(value: 1, timescale: 60) - let cgImage = try generator.copyCGImage(at: time, actualTime: nil) - let image = UIImage(cgImage: cgImage, scale: UIScreen.main.scale, orientation: .up) - return image - } - - static func resize(image: UIImage, maxDimensionPoints: CGFloat) -> UIImage? { - resize(image: image, originalSize: image.size, maxDimension: maxDimensionPoints, isPixels: false) - } - - static func resize(image: UIImage, maxDimensionPixels: CGFloat) -> UIImage? { - resize(image: image, originalSize: image.pixelSize, maxDimension: maxDimensionPixels, isPixels: true) - } - - /// Original size and maxDimension should both be in the same units, either points or pixels. - private static func resize(image: UIImage, originalSize: CGSize, maxDimension: CGFloat, isPixels: Bool) -> UIImage? { - if originalSize.width < 1 || originalSize.height < 1 { - Logger.error("Invalid original size: \(originalSize)") - return nil - } - - let maxOriginalDimension = max(originalSize.width, originalSize.height) - if maxOriginalDimension < maxDimension { - // Don't bother scaling an image that is already smaller than the max dimension. - return image - } - - var unroundedThumbnailSize: CGSize - if originalSize.width > originalSize.height { - unroundedThumbnailSize = CGSize(width: maxDimension, height: maxDimension * originalSize.height / originalSize.width) - } else { - unroundedThumbnailSize = CGSize(width: maxDimension * originalSize.width / originalSize.height, height: maxDimension) - } - - var renderRect = CGRect(origin: .zero, - size: CGSize.init(width: round(unroundedThumbnailSize.width), - height: round(unroundedThumbnailSize.height))) - if unroundedThumbnailSize.width < 1 { - // crop instead of resizing. - let newWidth = min(maxDimension, originalSize.width) - let newHeight = originalSize.height * (newWidth / originalSize.width) - renderRect.origin.y = round((maxDimension - newHeight) / 2) - renderRect.size.width = round(newWidth) - renderRect.size.height = round(newHeight) - unroundedThumbnailSize.height = maxDimension - unroundedThumbnailSize.width = newWidth - } - if unroundedThumbnailSize.height < 1 { - // crop instead of resizing. - let newHeight = min(maxDimension, originalSize.height) - let newWidth = originalSize.width * (newHeight / originalSize.height) - renderRect.origin.x = round((maxDimension - newWidth) / 2) - renderRect.size.width = round(newWidth) - renderRect.size.height = round(newHeight) - unroundedThumbnailSize.height = newHeight - unroundedThumbnailSize.width = maxDimension - } - - let thumbnailSize = CGSize(width: round(unroundedThumbnailSize.width), - height: round(unroundedThumbnailSize.height)) - - let format = UIGraphicsImageRendererFormat() - if isPixels { - format.scale = 1 - } - format.opaque = false - let renderer = UIGraphicsImageRenderer(size: thumbnailSize, format: format) - return renderer.image { context in - context.cgContext.interpolationQuality = .high - image.draw(in: renderRect) - } - } - } - - enum BlurHash { - static func computeBlurHashSync(for image: UIImage) throws -> String { - // Use a small thumbnail size; quality doesn't matter. This is important for perf. - var thumbnail: UIImage - let maxDimensionPixels: CGFloat = 200 - if image.pixelSize.width > maxDimensionPixels || image.pixelSize.height > maxDimensionPixels { - thumbnail = try OWSMediaUtils.thumbnail(forImage: image, maxDimensionPixels: maxDimensionPixels) - } else { - thumbnail = image - } - guard let normalized = Self.normalize(image: thumbnail, backgroundColor: .white) else { - throw OWSAssertionError("Could not normalize thumbnail.") - } - // blurHash uses a DCT transform, so these are AC and DC components. - // We use 4x3. - // - // https://github.com/woltapp/blurhash/blob/master/Algorithm.md - guard let blurHash = normalized.blurHash(numberOfComponents: (4, 3)) else { - throw OWSAssertionError("Could not generate blurHash.") - } - guard self.isValidBlurHash(blurHash) else { - throw OWSAssertionError("Generated invalid blurHash.") - } - return blurHash - } - - // BlurHashEncode only works with images in a very specific - // pixel format: RGBA8888. - static func normalize(image: UIImage, backgroundColor: UIColor) -> UIImage? { - guard let cgImage = image.cgImage else { - return nil - } - - // As long as we're normalizing the image, reduce the size. - // The blurHash algorithm doesn't need more data. - // This also places an upper bound on blurHash perf cost. - let srcSize = image.pixelSize - guard srcSize.width > 0, srcSize.height > 0 else { - return nil - } - let srcMinDimension: CGFloat = min(srcSize.width, srcSize.height) - // Make sure the short dimension is N. - let scale: CGFloat = min(1.0, 16 / srcMinDimension) - let dstWidth: Int = Int(round(srcSize.width * scale)) - let dstHeight: Int = Int(round(srcSize.height * scale)) - let dstSize = CGSize(width: dstWidth, height: dstHeight) - let dstRect = CGRect(origin: .zero, size: dstSize) - let colorSpace = CGColorSpaceCreateDeviceRGB() - // RGBA8888 pixel format - let bitmapInfo = CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.premultipliedLast.rawValue - guard let context = CGContext( - data: nil, - width: dstWidth, - height: dstHeight, - bitsPerComponent: 8, - bytesPerRow: dstWidth * 4, - space: colorSpace, - bitmapInfo: bitmapInfo - ) else { - return nil - } - context.setFillColor(backgroundColor.cgColor) - context.fill(dstRect) - context.draw(cgImage, in: dstRect) - return (context.makeImage().flatMap { UIImage(cgImage: $0) }) - } - - static func isValidBlurHash(_ blurHash: String?) -> Bool { - guard let blurHash = blurHash else { - return false - } - guard blurHash.count >= 6 && blurHash.count < 100 else { - return false - } - return blurHash.unicodeScalars.allSatisfy { - CharacterSet( - charactersIn: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" - ).contains($0) - } - } - } -} diff --git a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+Records.swift b/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+Records.swift deleted file mode 100644 index 8d130afed2..0000000000 --- a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+Records.swift +++ /dev/null @@ -1,914 +0,0 @@ -// -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -import Foundation -import GRDB - -extension TSAttachmentMigration { - - /// These are the "live" models this migration depends on. - /// We point to the same Swift class/struct model as the live app on the - /// assumption that they will need to always be backwards compatible regardless, - /// so using them here (which requires backwards compatibility) adds no new burden. - /// - /// If you are writing a migration to remove these models or update them in a - /// non-backwards compatible way, that migration likely needs to make copies of - /// the pre-migration models so that it knows how to read them before migrating them. - /// This migration should be updated to point at those new copies. - enum LiveModels { - typealias MessageBodyRanges = SignalServiceKit.MessageBodyRanges - typealias SignalServiceAddress = SignalServiceKit.SignalServiceAddress - typealias StyleOnlyMessageBody = SignalServiceKit.StyleOnlyMessageBody - } - - struct V1Attachment: Codable, MutablePersistableRecord, FetchableRecord { - static let databaseTableName: String = "model_TSAttachment" - - enum AttachmentType: Int, Codable, Equatable { - case `default` = 0 - case voiceMessage = 1 - case borderless = 2 - case gif = 3 - - var asRenderingFlag: TSAttachmentMigration.V2RenderingFlag { - switch self { - case .default: - return .default - case .voiceMessage: - return .voiceMessage - case .borderless: - return .borderless - case .gif: - return .shouldLoop - } - } - } - - static let attachmentPointerSDSRecordType: UInt32 = 3 - static let attachmentStreamSDSRecordType: UInt32 = 18 - static let attachmentSDSRecordType: UInt32 = 6 - - var id: Int64? - var recordType: UInt32 - var uniqueId: String - var albumMessageId: String? - var attachmentType: V1Attachment.AttachmentType - var blurHash: String? - var byteCount: UInt32 - var caption: String? - var contentType: String - var encryptionKey: Data? - var serverId: UInt64 - var sourceFilename: String? - var cachedAudioDurationSeconds: Double? - var cachedImageHeight: Double? - var cachedImageWidth: Double? - var creationTimestamp: Double? - var digest: Data? - var isUploaded: Bool? - var isValidImageCached: Bool? - var isValidVideoCached: Bool? - var lazyRestoreFragmentId: String? - var localRelativeFilePath: String? - var mediaSize: Data? - var pointerType: UInt? - var state: UInt32? - var uploadTimestamp: UInt64 - var cdnKey: String - var cdnNumber: UInt32 - var isAnimatedCached: Bool? - var attachmentSchemaVersion: UInt - var videoDuration: Double? - var clientUuid: String? - - func sourceMediaSizePixels() throws -> (height: UInt32, width: UInt32)? { - guard let encoded = mediaSize else { - return nil - } - guard - let decoded = try NSKeyedUnarchiver - .unarchiveTopLevelObjectWithData(encoded) as? CGSize - else { - throw OWSAssertionError("Invalid media size") - } - guard - let height = UInt32(exactly: decoded.height), - let width = UInt32(exactly: decoded.width) - else { - return nil - } - return (height, width) - } - - var localFilePath: String? { - guard let localRelativeFilePath else { - return nil - } - let rootPath = FileManager.default.containerURL( - forSecurityApplicationGroupIdentifier: TSConstants.applicationGroup - )!.path - let attachmentsFolder = rootPath.appendingPathComponent("Attachments") - return attachmentsFolder.appendingPathComponent(localRelativeFilePath) - } - - var thumbnailsDirPath: String { - let dirName = "\(uniqueId)-thumbnails" - return OWSFileSystem.cachesDirectoryPath().appendingPathComponent(dirName) - } - - var legacyThumbnailPath: String? { - guard let localRelativeFilePath else { - return nil - } - let filename = ((localRelativeFilePath as NSString).lastPathComponent as NSString).deletingPathExtension - let containingDir = (localRelativeFilePath as NSString).deletingLastPathComponent - let newFilename = filename.appending("-signal-ios-thumbnail") - return (containingDir.appendingPathComponent(newFilename) as NSString).appendingPathExtension("jpg") - } - - var uniqueIdAttachmentFolder: String { - let rootPath = FileManager.default.containerURL( - forSecurityApplicationGroupIdentifier: TSConstants.applicationGroup - )!.path - let attachmentsFolder = rootPath.appendingPathComponent("Attachments") - return attachmentsFolder.appendingPathComponent(self.uniqueId) - } - - func deleteFiles() throws { - // Ignore failure cuz its a cache directory anyway. - _ = OWSFileSystem.deleteFileIfExists(thumbnailsDirPath) - - if let legacyThumbnailPath { - guard OWSFileSystem.deleteFileIfExists(legacyThumbnailPath) else { - throw OWSAssertionError("Failed to delete file") - } - } - - if let localFilePath { - guard OWSFileSystem.deleteFileIfExists(localFilePath) else { - throw OWSAssertionError("Failed to delete file") - } - } - - guard OWSFileSystem.deleteFileIfExists(uniqueIdAttachmentFolder) else { - throw OWSAssertionError("Failed to delete folder") - } - } - - func deleteMediaGalleryRecord(tx: DBWriteTransaction) throws { - try tx.database.execute( - sql: "DELETE FROM media_gallery_items WHERE attachmentId = ?", - arguments: [self.id] - ) - } - } - - struct V1AttachmentReservedFileIds: Codable, MutablePersistableRecord, FetchableRecord { - static let databaseTableName: String = "TSAttachmentMigration" - - var tsAttachmentUniqueId: String - var interactionRowId: Int64? - var storyMessageRowId: Int64? - var reservedV2AttachmentPrimaryFileId: UUID - var reservedV2AttachmentAudioWaveformFileId: UUID - var reservedV2AttachmentVideoStillFrameFileId: UUID - - static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy = .deferredToUUID - - init( - tsAttachmentUniqueId: String, - interactionRowId: Int64?, - storyMessageRowId: Int64?, - reservedV2AttachmentPrimaryFileId: UUID, - reservedV2AttachmentAudioWaveformFileId: UUID, - reservedV2AttachmentVideoStillFrameFileId: UUID - ) { - self.tsAttachmentUniqueId = tsAttachmentUniqueId - self.interactionRowId = interactionRowId - self.storyMessageRowId = storyMessageRowId - self.reservedV2AttachmentPrimaryFileId = reservedV2AttachmentPrimaryFileId - self.reservedV2AttachmentAudioWaveformFileId = reservedV2AttachmentAudioWaveformFileId - self.reservedV2AttachmentVideoStillFrameFileId = reservedV2AttachmentVideoStillFrameFileId - } - - func cleanUpFiles() { - for uuid in [ - self.reservedV2AttachmentPrimaryFileId, - self.reservedV2AttachmentAudioWaveformFileId, - self.reservedV2AttachmentVideoStillFrameFileId - ] { - let relPath = TSAttachmentMigration.V2Attachment.relativeFilePath(reservedUUID: uuid) - let fileUrl = TSAttachmentMigration.V2Attachment.absoluteAttachmentFileURL( - relativeFilePath: relPath - ) - do { - try OWSFileSystem.deleteFileIfExists(url: fileUrl) - } catch { - owsFail("Unable to clean up reserved files") - } - } - } - } - - struct V2Attachment: Codable, MutablePersistableRecord, FetchableRecord { - static let databaseTableName: String = "Attachment" - - enum ContentType: Int { - case invalid = 0 - case file = 1 - case image = 2 - case video = 3 - case animatedImage = 4 - case audio = 5 - } - - var id: Int64? - var blurHash: String? - var sha256ContentHash: Data? - var encryptedByteCount: UInt32? - var unencryptedByteCount: UInt32? - var mimeType: String - var encryptionKey: Data - var digestSHA256Ciphertext: Data? - var contentType: UInt32? - var transitCdnNumber: UInt32? - var transitCdnKey: String? - var transitUploadTimestamp: UInt64? - var transitEncryptionKey: Data? - var transitUnencryptedByteCount: UInt32? - var transitDigestSHA256Ciphertext: Data? - var lastTransitDownloadAttemptTimestamp: UInt64? - var mediaName: String? - var mediaTierCdnNumber: UInt32? - var mediaTierUnencryptedByteCount: UInt32? - var mediaTierUploadEra: String? - var lastMediaTierDownloadAttemptTimestamp: UInt64? - var thumbnailCdnNumber: UInt32? - var thumbnailUploadEra: String? - var lastThumbnailDownloadAttemptTimestamp: UInt64? - var localRelativeFilePath: String? - var localRelativeFilePathThumbnail: String? - var cachedAudioDurationSeconds: Double? - var cachedMediaHeightPixels: UInt32? - var cachedMediaWidthPixels: UInt32? - var cachedVideoDurationSeconds: Double? - var audioWaveformRelativeFilePath: String? - var videoStillFrameRelativeFilePath: String? - var originalAttachmentIdForQuotedReply: Int64? - - init( - id: Int64? = nil, - blurHash: String?, - sha256ContentHash: Data?, - encryptedByteCount: UInt32?, - unencryptedByteCount: UInt32?, - mimeType: String, - encryptionKey: Data, - digestSHA256Ciphertext: Data?, - contentType: UInt32?, - transitCdnNumber: UInt32?, - transitCdnKey: String?, - transitUploadTimestamp: UInt64?, - transitEncryptionKey: Data?, - transitUnencryptedByteCount: UInt32?, - transitDigestSHA256Ciphertext: Data?, - lastTransitDownloadAttemptTimestamp: UInt64?, - localRelativeFilePath: String?, - cachedAudioDurationSeconds: Double?, - cachedMediaHeightPixels: UInt32?, - cachedMediaWidthPixels: UInt32?, - cachedVideoDurationSeconds: Double?, - audioWaveformRelativeFilePath: String?, - videoStillFrameRelativeFilePath: String? - ) { - self.id = id - self.blurHash = blurHash - self.sha256ContentHash = sha256ContentHash - self.encryptedByteCount = encryptedByteCount - self.unencryptedByteCount = unencryptedByteCount - self.mimeType = mimeType - self.encryptionKey = encryptionKey - self.digestSHA256Ciphertext = digestSHA256Ciphertext - self.contentType = contentType - - // We only set transit tier fields if they are all set. - if - let transitCdnNumber, - transitCdnNumber != 0, - let transitCdnKey = transitCdnKey?.nilIfEmpty, - let transitEncryptionKey, - !transitEncryptionKey.isEmpty, - let transitUnencryptedByteCount, - let transitDigestSHA256Ciphertext, - !transitDigestSHA256Ciphertext.isEmpty - { - self.transitCdnNumber = transitCdnNumber - self.transitCdnKey = transitCdnKey - self.transitUploadTimestamp = transitUploadTimestamp ?? Date().ows_millisecondsSince1970 - self.transitEncryptionKey = transitEncryptionKey - self.transitUnencryptedByteCount = transitUnencryptedByteCount - self.transitDigestSHA256Ciphertext = transitDigestSHA256Ciphertext - } else { - self.transitCdnNumber = nil - self.transitCdnKey = nil - self.transitUploadTimestamp = nil - self.transitEncryptionKey = nil - self.transitUnencryptedByteCount = nil - self.transitDigestSHA256Ciphertext = nil - } - self.lastTransitDownloadAttemptTimestamp = lastTransitDownloadAttemptTimestamp - self.mediaName = digestSHA256Ciphertext.map { - TSAttachmentMigration.V2Attachment.mediaName( - digestSHA256Ciphertext: $0 - ) - } - // Media tier and thumbnail upload info was unsupported in TSAttachment - // and therefore will always be nil in this migration. - self.mediaTierCdnNumber = nil - self.mediaTierUnencryptedByteCount = nil - self.mediaTierUploadEra = nil - self.lastMediaTierDownloadAttemptTimestamp = nil - self.thumbnailCdnNumber = nil - self.thumbnailUploadEra = nil - self.lastThumbnailDownloadAttemptTimestamp = nil - self.localRelativeFilePath = localRelativeFilePath - self.localRelativeFilePathThumbnail = nil - self.cachedAudioDurationSeconds = cachedAudioDurationSeconds - self.cachedMediaHeightPixels = cachedMediaHeightPixels - self.cachedMediaWidthPixels = cachedMediaWidthPixels - self.cachedVideoDurationSeconds = cachedVideoDurationSeconds - self.audioWaveformRelativeFilePath = audioWaveformRelativeFilePath - self.videoStillFrameRelativeFilePath = videoStillFrameRelativeFilePath - // In the migration we never reference a quote original since the original - // is likely unmigrated when we migrate the quoted reply. - self.originalAttachmentIdForQuotedReply = nil - } - - mutating func didInsert(with rowID: Int64, for column: String?) { - self.id = rowID - } - - static func relativeFilePath(reservedUUID: UUID) -> String { - let id = reservedUUID.uuidString - return "\(id.prefix(2))/\(id)" - } - - static func absoluteAttachmentFileURL(relativeFilePath: String) -> URL { - let rootUrl = FileManager.default.containerURL( - forSecurityApplicationGroupIdentifier: TSConstants.applicationGroup - )! - let directory = rootUrl.appendingPathComponent("attachment_files") - return directory.appendingPathComponent(relativeFilePath) - } - - static func mediaName(digestSHA256Ciphertext: Data) -> String { - return digestSHA256Ciphertext.hexadecimalString - } - } - - enum V2RenderingFlag: Int { - case `default` = 0 - case voiceMessage = 1 - case borderless = 2 - case shouldLoop = 3 - } - - enum V2MessageAttachmentOwnerType: Int { - case bodyAttachment = 0 - case oversizeText = 1 - case linkPreview = 2 - case quotedReplyAttachment = 3 - case sticker = 4 - case contactAvatar = 5 - } - - struct MessageAttachmentReference: Codable, PersistableRecord, FetchableRecord { - static let databaseTableName: String = "MessageAttachmentReference" - - var ownerType: UInt32 - var ownerRowId: Int64 - var attachmentRowId: Int64 - var receivedAtTimestamp: UInt64 - var contentType: UInt32? - var renderingFlag: UInt32 - var idInMessage: String? - var orderInMessage: UInt32? - var threadRowId: Int64 - var caption: String? - var sourceFilename: String? - var sourceUnencryptedByteCount: UInt32? - var sourceMediaHeightPixels: UInt32? - var sourceMediaWidthPixels: UInt32? - var stickerPackId: Data? - var stickerId: UInt32? - var isViewOnce: Bool - var ownerIsPastEditRevision: Bool - } - - struct StoryMessageAttachmentReference: Codable, PersistableRecord, FetchableRecord { - static let databaseTableName: String = "StoryMessageAttachmentReference" - - var ownerType: UInt32 - var ownerRowId: Int64 - var attachmentRowId: Int64 - var shouldLoop: Bool - var caption: String? - var captionBodyRanges: Data? - var sourceFilename: String? - var sourceUnencryptedByteCount: UInt32? - var sourceMediaHeightPixels: UInt32? - var sourceMediaWidthPixels: UInt32? - } - - struct ThreadAttachmentReference: Codable, PersistableRecord, FetchableRecord { - static let databaseTableName: String = "ThreadAttachmentReference" - - var ownerRowId: Int64? - var attachmentRowId: Int64 - var creationTimestamp: UInt64 - } - - // MARK: - MTLModels - - private static let nsCodingMappings: [String: AnyClass] = [ - "SignalServiceKit.OWSLinkPreview": TSAttachmentMigration.OWSLinkPreview.self, - "StickerInfo": TSAttachmentMigration.StickerInfo.self, - "SignalServiceKit.MessageSticker": TSAttachmentMigration.MessageSticker.self, - "OWSContact": TSAttachmentMigration.OWSContact.self, - "OWSContactAddress": TSAttachmentMigration.OWSContactAddress.self, - "OWSContactEmail": TSAttachmentMigration.OWSContactEmail.self, - "OWSContactName": TSAttachmentMigration.OWSContactName.self, - "OWSContactPhoneNumber": TSAttachmentMigration.OWSContactPhoneNumber.self, - "OWSAttachmentInfo": TSAttachmentMigration.OWSAttachmentInfo.self, - "TSQuotedMessage": TSAttachmentMigration.TSQuotedMessage.self, - "SignalServiceKit.MessageBodyRanges": LiveModels.MessageBodyRanges.self, - "SignalServiceKit.SignalServiceAddress": LiveModels.SignalServiceAddress.self, - ] - - static func prepareNSCodingMappings(archiver: NSKeyedArchiver) { - Self.nsCodingMappings.forEach { originalClassName, migrationClass in - archiver.setClassName(originalClassName, for: migrationClass) - } - } - - static func prepareNSCodingMappings(unarchiver: NSKeyedUnarchiver) { - Self.nsCodingMappings.forEach { originalClassName, migrationClass in - unarchiver.setClass(migrationClass, forClassName: originalClassName) - } - } - - static func cleanUpNSCodingMappings(archiver: NSKeyedArchiver) { - Self.nsCodingMappings.forEach { originalClassName, migrationClass in - archiver.setClassName(nil, for: migrationClass) - } - } - - static func cleanUpNSCodingMappings(unarchiver: NSKeyedUnarchiver) { - Self.nsCodingMappings.forEach { originalClassName, migrationClass in - unarchiver.setClass(nil, forClassName: originalClassName) - } - } - - @objcMembers - class OWSLinkPreview: MTLModel, Codable { - var urlString: String? - var title: String? - var imageAttachmentId: String? - var usesV2AttachmentReferenceValue: NSNumber? - var previewDescription: String? - var date: Date? - - override init() { super.init() } - - required init!(coder: NSCoder) { super.init(coder: coder) } - - required init(dictionary dictionaryValue: [String: Any]!) throws { - try super.init(dictionary: dictionaryValue) - } - - enum CodingKeys: String, CodingKey { - case urlString - case title - case usesV2AttachmentReferenceValue - case imageAttachmentId - case previewDescription - case date - } - - required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - urlString = try container.decodeIfPresent(String.self, forKey: .urlString) - title = try container.decodeIfPresent(String.self, forKey: .title) - let usesV2AttachmentReferenceValue = try container.decodeIfPresent(Int.self, forKey: .usesV2AttachmentReferenceValue) - self.usesV2AttachmentReferenceValue = usesV2AttachmentReferenceValue.map(NSNumber.init(integerLiteral:)) - imageAttachmentId = try container.decodeIfPresent(String.self, forKey: .imageAttachmentId) - previewDescription = try container.decodeIfPresent(String.self, forKey: .previewDescription) - date = try container.decodeIfPresent(Date.self, forKey: .date) - super.init() - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(urlString, forKey: .urlString) - try container.encodeIfPresent(title, forKey: .title) - try container.encodeIfPresent(usesV2AttachmentReferenceValue?.intValue, forKey: .usesV2AttachmentReferenceValue) - try container.encodeIfPresent(imageAttachmentId, forKey: .imageAttachmentId) - try container.encodeIfPresent(previewDescription, forKey: .previewDescription) - try container.encodeIfPresent(date, forKey: .date) - } - } - - @objcMembers - class StickerInfo: MTLModel { - var packId: Data = Randomness.generateRandomBytes(16) - var packKey: Data = Randomness.generateRandomBytes(32) - var stickerId: UInt32 = 0 - - override init() { super.init() } - - required init!(coder: NSCoder!) { super.init(coder: coder) } - - required init(dictionary: [String: Any]!) throws { - try super.init(dictionary: dictionary) - } - } - - @objcMembers - class MessageSticker: MTLModel { - var info = TSAttachmentMigration.StickerInfo() - var attachmentId: String? - var emoji: String? - - override init() { super.init() } - - required init!(coder: NSCoder) { super.init(coder: coder) } - - required init(dictionary dictionaryValue: [String: Any]!) throws { - try super.init(dictionary: dictionaryValue) - } - } - - @objcMembers - public class OWSContactName: MTLModel { - var givenName: String? - var familyName: String? - var namePrefix: String? - var nameSuffix: String? - var middleName: String? - var nickname: String? - var organizationName: String? - - override init() { super.init() } - - required init!(coder: NSCoder!) { super.init(coder: coder) } - - required init(dictionary dictionaryValue: [String: Any]!) throws { - try super.init(dictionary: dictionaryValue) - } - } - - @objcMembers - class OWSContactPhoneNumber: MTLModel { - @objc - enum `Type`: Int { - case home = 1 - case mobile - case work - case custom - } - - var phoneType: `Type` = .home - var label: String? - var phoneNumber: String = "" - - override init() { super.init() } - - required init!(coder: NSCoder!) { super.init(coder: coder) } - - required init(dictionary dictionaryValue: [String: Any]!) throws { - try super.init(dictionary: dictionaryValue) - } - } - - @objcMembers - class OWSContactEmail: MTLModel { - @objc - enum `Type`: Int { - case home = 1 - case mobile - case work - case custom - } - - var emailType: `Type` = .home - var label: String? - var email: String = "" - - override init() { super.init() } - - required init!(coder: NSCoder!) { super.init(coder: coder) } - - required init(dictionary dictionaryValue: [String: Any]!) throws { - try super.init(dictionary: dictionaryValue) - } - } - - @objcMembers - class OWSContactAddress: MTLModel { - @objc - enum `Type`: Int { - case home = 1 - case work - case custom - } - - var addressType: `Type` = .home - var label: String? - var street: String? - var pobox: String? - var neighborhood: String? - var city: String? - var region: String? - var postcode: String? - var country: String? - - override init() { super.init() } - - required init!(coder: NSCoder!) { super.init(coder: coder) } - - required init(dictionary dictionaryValue: [String: Any]!) throws { - try super.init(dictionary: dictionaryValue) - } - } - - @objcMembers - class OWSContact: MTLModel { - var name: TSAttachmentMigration.OWSContactName - var phoneNumbers: [TSAttachmentMigration.OWSContactPhoneNumber] = [] - var emails: [TSAttachmentMigration.OWSContactEmail] = [] - var addresses: [TSAttachmentMigration.OWSContactAddress] = [] - var avatarAttachmentId: String? - - override init() { - self.name = TSAttachmentMigration.OWSContactName() - super.init() - } - - required init!(coder: NSCoder!) { - self.name = TSAttachmentMigration.OWSContactName() - super.init(coder: coder) - } - - required init(dictionary dictionaryValue: [String: Any]!) throws { - self.name = TSAttachmentMigration.OWSContactName() - try super.init(dictionary: dictionaryValue) - } - } - - @objc - enum OWSAttachmentInfoReference: Int, Codable { - case unset = 0 - case originalForSend = 1 - case original = 2 - case thumbnail = 3 - case untrustedPointer = 4 - case v2 = 5 - } - - @objcMembers - class OWSAttachmentInfo: MTLModel, NSSecureCoding { - var schemaVersion: UInt = 1 - var attachmentType: TSAttachmentMigration.OWSAttachmentInfoReference = .unset - var rawAttachmentId: String = "" - var contentType: String? - var sourceFilename: String? - - static var supportsSecureCoding: Bool = false - - init( - schemaVersion: UInt = 1, - attachmentType: TSAttachmentMigration.OWSAttachmentInfoReference, - rawAttachmentId: String, - contentType: String?, - sourceFilename: String? - ) { - self.schemaVersion = schemaVersion - self.attachmentType = attachmentType - self.rawAttachmentId = rawAttachmentId - self.contentType = contentType - self.sourceFilename = sourceFilename - super.init() - } - - override init() { super.init() } - - required init!(coder: NSCoder!) { - super.init(coder: coder) - - if schemaVersion == 0 { - let oldStreamId = coder.decodeObject(of: NSString.self, forKey: "thumbnailAttachmentStreamId") - let oldPointerId = coder.decodeObject(of: NSString.self, forKey: "thumbnailAttachmentPointerId") - let oldSourceAttachmentId = coder.decodeObject(of: NSString.self, forKey: "attachmentId") - - // Before, we maintained each of these IDs in parallel, though in practice only one in use at a time. - // Migration codifies this behavior. - if let oldStreamId, oldPointerId == oldStreamId { - attachmentType = .thumbnail - rawAttachmentId = oldStreamId as String - } else if let oldPointerId { - attachmentType = .untrustedPointer - rawAttachmentId = oldPointerId as String - } else if let oldStreamId { - attachmentType = .thumbnail - rawAttachmentId = oldStreamId as String - } else if let oldSourceAttachmentId { - attachmentType = .originalForSend - rawAttachmentId = oldSourceAttachmentId as String - } else { - attachmentType = .unset - rawAttachmentId = "" - } - } - self.schemaVersion = 1 - } - - required init(dictionary dictionaryValue: [String: Any]!) throws { - try super.init(dictionary: dictionaryValue) - } - } - - @objc - enum TSQuotedMessageContentSource: Int, Codable { - case unknown = 0 - case local = 1 - case remote = 2 - case story = 3 - } - - @objcMembers - class TSQuotedMessage: MTLModel { - var timestamp: UInt64 = 0 - var authorAddress: LiveModels.SignalServiceAddress? - var bodySource: TSAttachmentMigration.TSQuotedMessageContentSource = .unknown - var body: String? - var bodyRanges: LiveModels.MessageBodyRanges? - var quotedAttachment: TSAttachmentMigration.OWSAttachmentInfo? - var isGiftBadge: Bool = false - - override init() { super.init() } - - required init!(coder: NSCoder!) { - super.init(coder: coder) - - if authorAddress == nil, let phoneNumber = coder.decodeObject(of: NSString.self, forKey: "authorId") { - authorAddress = LiveModels.SignalServiceAddress.legacyAddress(aciString: nil, phoneNumber: phoneNumber as String) - } - - if - quotedAttachment == nil, - let array = coder.decodeObject(of: NSArray.self, forKey: "quotedAttachments"), - let first = array.firstObject as? TSAttachmentMigration.OWSAttachmentInfo - { - quotedAttachment = first - } else if - quotedAttachment == nil, - let quotedAttachment = coder.decodeObject(of: TSAttachmentMigration.OWSAttachmentInfo.self, forKey: "quotedAttachments") - { - self.quotedAttachment = quotedAttachment - } - } - - required init(dictionary dictionaryValue: [String: Any]!) throws { - try super.init(dictionary: dictionaryValue) - } - } - - // MARK: - Styles - - struct NSRangedValue { - let range: NSRange - let value: T - } - - struct Style: OptionSet, Codable { - let rawValue: Int - - init(rawValue: Int) { - self.rawValue = rawValue - } - } - - enum SingleStyle: Int, Codable { - case bold = 1 - case italic = 2 - case spoiler = 4 - case strikethrough = 8 - case monospace = 16 - } - - struct MergedSingleStyle: Equatable, Codable { - let style: TSAttachmentMigration.SingleStyle - let mergedRange: NSRange - let id: Int - } - - struct CollapsedStyle: Equatable, Codable { - let style: TSAttachmentMigration.Style - let originals: [TSAttachmentMigration.SingleStyle: TSAttachmentMigration.MergedSingleStyle] - } - - // MARK: - Stories - - enum SerializedStoryMessageAttachment: Codable { - case file(attachmentId: String) - case text(attachment: TSAttachmentMigration.TextAttachment) - case fileV2(TSAttachmentMigration.StoryMessageFileAttachment) - case foreignReferenceAttachment - } - - struct StoryMessageFileAttachment: Codable { - let attachmentId: String - let captionStyles: [TSAttachmentMigration.NSRangedValue] - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.attachmentId = try container.decode(String.self, forKey: .attachmentId) - - do { - // A year prior to this migration being written, captionStyles contained raw Styles - // instead of collapsed styles. Stories expire in 24 hours. Byt the time of this - // migration any story with the non-collapsed style is expired; technically though - // this migration runs before StoryManager deletes expired stories. So we need to not - // fail, but its ok to drop the caption styles since its about to be deleted anyway. - self.captionStyles = try container.decode([TSAttachmentMigration.NSRangedValue].self, forKey: .captionStyles) - } catch { - self.captionStyles = [] - } - } - } - - struct TextAttachment: Codable, Equatable { - - enum TextStyle: Int, Codable, Equatable { - case regular = 0 - case bold = 1 - case serif = 2 - case script = 3 - case condensed = 4 - } - - enum RawBackground: Codable, Equatable { - case color(hex: UInt32) - case gradient(raw: Self.RawGradient) - - struct RawGradient: Codable, Equatable { - let colors: [UInt32] - let positions: [Float] - let angle: UInt32 - } - } - - let body: LiveModels.StyleOnlyMessageBody? - let textStyle: Self.TextStyle - var preview: TSAttachmentMigration.OWSLinkPreview? - let textForegroundColorHex: UInt32? - let textBackgroundColorHex: UInt32? - let rawBackground: Self.RawBackground - - enum CodingKeys: String, CodingKey { - case body = "text" - case textStyle - case textForegroundColorHex - case textBackgroundColorHex - case rawBackground - case preview - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - do { - // Backwards compability; this used to contain just a raw string, - // which we now interpret as a style-less string. - if let rawText = try container.decodeIfPresent(String.self, forKey: .body) { - self.body = LiveModels.StyleOnlyMessageBody(plaintext: rawText) - } else { - self.body = nil - } - } catch { - self.body = try container.decodeIfPresent(LiveModels.StyleOnlyMessageBody.self, forKey: .body) - } - - self.textStyle = try container.decode(Self.TextStyle.self, forKey: .textStyle) - self.textForegroundColorHex = try container.decodeIfPresent(UInt32.self, forKey: .textForegroundColorHex) - self.textBackgroundColorHex = try container.decodeIfPresent(UInt32.self, forKey: .textBackgroundColorHex) - self.rawBackground = try container.decode(Self.RawBackground.self, forKey: .rawBackground) - self.preview = try container.decodeIfPresent(TSAttachmentMigration.OWSLinkPreview.self, forKey: .preview) - } - } -} - -extension TSAttachmentMigration.NSRangedValue: Codable where T: Codable {} diff --git a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+StoryMessageAttachment.swift b/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+StoryMessageAttachment.swift deleted file mode 100644 index 2af8bae5f1..0000000000 --- a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+StoryMessageAttachment.swift +++ /dev/null @@ -1,316 +0,0 @@ -// -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -import Foundation -import GRDB - -extension TSAttachmentMigration { - - /// Migration for StoryMessage-owned TSAttachments. - /// After migrating they are v2 attachments in the StoryMessageAttachmentReference table. - /// - /// The migration works in two passes which must happen in separate write transactions but - /// must be run back to back. (Why? Filesystem changes are not part of the db transaction, - /// so we need to first "reserve" the final file location in the db and then write the file. If the latter - /// step fails we will just rewrite to the same file location next time we retry.) - /// - /// Phase 1: Read the StoryMessage table, for each story message create - /// a TSAttachmentMigration row with the "reserved" (random) final file location. - /// - /// Phase 2: for each reserved file location, do attachment validation and create the v2 attachments. - /// - /// This migration is run up front as a blocking GRDB migration, because most people have very - /// few story messages so its not worth the complexity to run this incrementally. - enum StoryMessageMigration { - - /// Phase 1 - static func prepareStoryMessageMigration(tx: DBWriteTransaction) throws { - let storyMessageCursor = try Row.fetchCursor( - tx.database, - sql: "SELECT id, attachment FROM model_StoryMessage" - ) - // The `attachment` column is a SerializedStoryMessageAttachment encoded as a JSON string. - let decoder = JSONDecoder() - while let storyMessageRow = try storyMessageCursor.next() { - guard - let storyMessageRowId = storyMessageRow["id"] as? Int64, - let storyAttachmentString = storyMessageRow["attachment"] as? String - else { - throw OWSAssertionError("Unexpected row format") - } - let storyMessageAttachmentData = Data(storyAttachmentString.utf8) - let storyAttachment = try decoder.decode( - TSAttachmentMigration.SerializedStoryMessageAttachment.self, - from: storyMessageAttachmentData - ) - guard let tsAttachmentUniqueId = storyAttachment.tsAttachmentUniqueId else { continue } - - var reservedFileIds = TSAttachmentMigration.V1AttachmentReservedFileIds( - tsAttachmentUniqueId: tsAttachmentUniqueId, - interactionRowId: nil, - storyMessageRowId: storyMessageRowId, - reservedV2AttachmentPrimaryFileId: UUID(), - reservedV2AttachmentAudioWaveformFileId: UUID(), - reservedV2AttachmentVideoStillFrameFileId: UUID() - ) - try reservedFileIds.insert(tx.database) - } - } - - /// Phase 2 - static func completeStoryMessageMigration(tx: DBWriteTransaction) throws { - let decoder = JSONDecoder() - let encoder = JSONEncoder() - let reservedFileIdsCursor = try TSAttachmentMigration.V1AttachmentReservedFileIds - .filter(Column("storyMessageRowId") != nil) - .fetchCursor(tx.database) - var deletedAttachments = [TSAttachmentMigration.V1Attachment]() - while let reservedFileIds = try reservedFileIdsCursor.next() { - guard let storyMessageRowId = reservedFileIds.storyMessageRowId else { - continue - } - let storyAttachmentString = try String.fetchOne( - tx.database, - sql: "SELECT attachment FROM model_StoryMessage WHERE id = ?;", - arguments: [storyMessageRowId] - ) - guard let storyAttachmentString else { - reservedFileIds.cleanUpFiles() - continue - } - let storyMessageAttachmentData = Data(storyAttachmentString.utf8) - // The `attachment` column is a SerializedStoryMessageAttachment encoded as a JSON string. - let storyAttachment = try decoder.decode( - TSAttachmentMigration.SerializedStoryMessageAttachment.self, - from: storyMessageAttachmentData - ) - guard let tsAttachmentUniqueId = storyAttachment.tsAttachmentUniqueId else { - reservedFileIds.cleanUpFiles() - continue - } - try Self.migrateStoryMessageAttachment( - reservedFileIds: reservedFileIds, - storyAttachment: storyAttachment, - storyMessageRowId: storyMessageRowId, - tsAttachmentUniqueId: tsAttachmentUniqueId, - tx: tx - ) - // Update the story message. - let updatedStoryAttachment: TSAttachmentMigration.SerializedStoryMessageAttachment = { - switch storyAttachment { - case .file, .fileV2, .foreignReferenceAttachment: - return .foreignReferenceAttachment - case .text(var textAttachment): - let preview = textAttachment.preview - preview?.imageAttachmentId = nil - preview?.usesV2AttachmentReferenceValue = NSNumber(value: true) - textAttachment.preview = preview - return .text(attachment: textAttachment) - } - }() - let updatedStoryAttachmentRaw = try encoder.encode(updatedStoryAttachment) - try tx.database.execute( - sql: """ - UPDATE model_StoryMessage - SET attachment = ? - WHERE id = ?; - """, - arguments: [updatedStoryAttachmentRaw, storyMessageRowId] - ) - - // Delete the attachment. - let deletedAttachment = try TSAttachmentMigration.V1Attachment.fetchOne( - tx.database, - sql: "DELETE FROM model_TSAttachment WHERE uniqueId = ? RETURNING *", - arguments: [tsAttachmentUniqueId] - ) - deletedAttachment.map { deletedAttachments.append($0) } - } - - // Delete our reserved rows. - try TSAttachmentMigration.V1AttachmentReservedFileIds - .filter(Column("storyMessageRowId") != nil) - .deleteAll(tx.database) - - tx.addSyncCompletion { - Task { - // Delete the files asynchronously after committing the tx. We can't do it - // inside the tx because if the tx is rolled back we DON'T want the files gone. - // This does mean we might fail to delete the files; we will delete the whole - // TSAttachment folder after migrating everything anyway so its not a huge deal. - deletedAttachments.forEach { try? $0.deleteFiles() } - } - } - } - - /// Migrates a single story message's attachment. - private static func migrateStoryMessageAttachment( - reservedFileIds: TSAttachmentMigration.V1AttachmentReservedFileIds, - storyAttachment: TSAttachmentMigration.SerializedStoryMessageAttachment, - storyMessageRowId: Int64, - tsAttachmentUniqueId: String, - tx: DBWriteTransaction - ) throws { - let oldAttachment = try TSAttachmentMigration.V1Attachment - .filter(Column("uniqueId") == tsAttachmentUniqueId) - .fetchOne(tx.database) - guard let oldAttachment else { - reservedFileIds.cleanUpFiles() - return - } - - let renderingFlag = oldAttachment.attachmentType.asRenderingFlag - - let pendingAttachment: TSAttachmentMigration.PendingV2AttachmentFile? - if let oldFilePath = oldAttachment.localFilePath { - do { - pendingAttachment = try TSAttachmentMigration.V2AttachmentContentValidator.validateContents( - unencryptedFileUrl: URL(fileURLWithPath: oldFilePath), - reservedFileIds: .init( - primaryFile: reservedFileIds.reservedV2AttachmentPrimaryFileId, - audioWaveform: reservedFileIds.reservedV2AttachmentAudioWaveformFileId, - videoStillFrame: reservedFileIds.reservedV2AttachmentVideoStillFrameFileId - ), - attachmentKey: oldAttachment.encryptionKey.map(AttachmentKey.init(combinedKey:)), - mimeType: oldAttachment.contentType, - renderingFlag: renderingFlag, - sourceFilename: oldAttachment.sourceFilename - ) - } catch { - Logger.error("Failed to read story attachment file \((error as NSError).domain) \((error as NSError).code)") - // Clean up files just in case. - reservedFileIds.cleanUpFiles() - pendingAttachment = nil - } - } else { - // A pointer; no validation needed. - pendingAttachment = nil - // Clean up files just in case. - reservedFileIds.cleanUpFiles() - } - - let v2AttachmentId: Int64 - if - let pendingAttachment, - let existingV2Attachment = try TSAttachmentMigration.V2Attachment - .filter(Column("sha256ContentHash") == pendingAttachment.sha256ContentHash) - .fetchOne(tx.database) - { - // If we already have a v2 attachment with the same plaintext hash, - // create new references to it and drop the pending attachment. - v2AttachmentId = existingV2Attachment.id! - // Delete the reserved files being used by the pending attachment. - reservedFileIds.cleanUpFiles() - } else { - var v2Attachment: TSAttachmentMigration.V2Attachment - if let pendingAttachment { - v2Attachment = TSAttachmentMigration.V2Attachment( - blurHash: pendingAttachment.blurHash, - sha256ContentHash: pendingAttachment.sha256ContentHash, - encryptedByteCount: pendingAttachment.encryptedByteCount, - unencryptedByteCount: pendingAttachment.unencryptedByteCount, - mimeType: pendingAttachment.mimeType, - encryptionKey: pendingAttachment.encryptionKey, - digestSHA256Ciphertext: pendingAttachment.digestSHA256Ciphertext, - contentType: UInt32(pendingAttachment.validatedContentType.rawValue), - transitCdnNumber: oldAttachment.cdnNumber, - transitCdnKey: oldAttachment.cdnKey, - transitUploadTimestamp: oldAttachment.uploadTimestamp, - transitEncryptionKey: oldAttachment.encryptionKey, - transitUnencryptedByteCount: pendingAttachment.unencryptedByteCount, - transitDigestSHA256Ciphertext: oldAttachment.digest, - lastTransitDownloadAttemptTimestamp: nil, - localRelativeFilePath: pendingAttachment.localRelativeFilePath, - cachedAudioDurationSeconds: pendingAttachment.audioDurationSeconds, - cachedMediaHeightPixels: pendingAttachment.mediaSizePixels.map { UInt32($0.height) }, - cachedMediaWidthPixels: pendingAttachment.mediaSizePixels.map { UInt32($0.width) }, - cachedVideoDurationSeconds: pendingAttachment.videoDurationSeconds, - audioWaveformRelativeFilePath: pendingAttachment.audioWaveformRelativeFilePath, - videoStillFrameRelativeFilePath: pendingAttachment.videoStillFrameRelativeFilePath - ) - } else { - v2Attachment = TSAttachmentMigration.V2Attachment( - blurHash: oldAttachment.blurHash, - sha256ContentHash: nil, - encryptedByteCount: nil, - unencryptedByteCount: nil, - mimeType: oldAttachment.contentType, - encryptionKey: oldAttachment.encryptionKey ?? AttachmentKey.generate().combinedKey, - digestSHA256Ciphertext: nil, - contentType: nil, - transitCdnNumber: oldAttachment.cdnNumber, - transitCdnKey: oldAttachment.cdnKey, - transitUploadTimestamp: oldAttachment.uploadTimestamp, - transitEncryptionKey: oldAttachment.encryptionKey, - transitUnencryptedByteCount: oldAttachment.byteCount, - transitDigestSHA256Ciphertext: oldAttachment.digest, - lastTransitDownloadAttemptTimestamp: nil, - localRelativeFilePath: pendingAttachment?.localRelativeFilePath, - cachedAudioDurationSeconds: nil, - cachedMediaHeightPixels: nil, - cachedMediaWidthPixels: nil, - cachedVideoDurationSeconds: nil, - audioWaveformRelativeFilePath: nil, - videoStillFrameRelativeFilePath: nil - ) - } - - try v2Attachment.insert(tx.database) - v2AttachmentId = v2Attachment.id! - } - - let mediaStoryOwnerType: UInt32 = 0 - let textStoryOwnerType: UInt32 = 1 - - let ownerType: UInt32 - let captionBodyRanges: [TSAttachmentMigration.NSRangedValue]? - switch storyAttachment { - case .file: - ownerType = mediaStoryOwnerType - captionBodyRanges = nil - case .fileV2(let fileAttachment): - ownerType = mediaStoryOwnerType - captionBodyRanges = fileAttachment.captionStyles - case .text(_): - ownerType = textStoryOwnerType - captionBodyRanges = nil - case .foreignReferenceAttachment: - return - } - - let (sourceMediaHeightPixels, sourceMediaWidthPixels) = try oldAttachment.sourceMediaSizePixels() ?? (nil, nil) - - let reference = TSAttachmentMigration.StoryMessageAttachmentReference( - ownerType: ownerType, - ownerRowId: storyMessageRowId, - attachmentRowId: v2AttachmentId, - shouldLoop: renderingFlag == .shouldLoop, - caption: oldAttachment.caption, - captionBodyRanges: try captionBodyRanges.map { try JSONEncoder().encode($0) }, - sourceFilename: oldAttachment.sourceFilename, - sourceUnencryptedByteCount: oldAttachment.byteCount, - sourceMediaHeightPixels: sourceMediaHeightPixels, - sourceMediaWidthPixels: sourceMediaWidthPixels - ) - try reference.insert(tx.database) - } - } -} - -extension TSAttachmentMigration.SerializedStoryMessageAttachment { - - var tsAttachmentUniqueId: String? { - switch self { - case .file(let attachmentId): - return attachmentId - case .text(let textAttachment): - return textAttachment.preview?.imageAttachmentId - case .fileV2(let attachment): - return attachment.attachmentId - case .foreignReferenceAttachment: - return nil - } - } -} diff --git a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+TSMessage.swift b/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+TSMessage.swift deleted file mode 100644 index e1cdd33bc4..0000000000 --- a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+TSMessage.swift +++ /dev/null @@ -1,1601 +0,0 @@ -// -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -import Foundation -import GRDB - -public protocol TSAttachmentMigrationLogger { - - /// An error occurred that caused the migration to abort. - /// Typically, persist the error to be logged across process launches. - func didFatalError(_ logString: String) - - /// A db corruption error occurred. - func flagDBCorrupted() - - /// Logging everything the migration does would be very noisy. - /// Instead, we "checkpoint" every once in a while, and can persist - /// the latest checkpoint. If the process terminates with a crash, - /// we can then log the last checkpoint we reached on next app launch, - /// giving a hint as to what mightv'e crashed even if we don't have a trace. - func checkpoint(_ checkpointString: String) -} - -extension TSAttachmentMigrationLogger { - - func checkpoint( - file: String = #fileID, - function: String = #function, - line: Int = #line - ) { - self.checkpoint("\(file):\(function):\(line)") - } -} - -extension TSAttachmentMigration { - - /// Migrate TSMessage TSAttachments to v2 Attachments and MessageAttachmentReferences. - /// - /// The migration works in 4 phases, and can be run either incrementally (while the app is running - /// or backgrounded) or as a blocking GRDB migration. - /// - /// The 4 phases must be broken up into at least 3 db transactions. (Why? Filesystem changes are - /// not part of the db transaction, so we need to first "reserve" the final file location in the db and then - /// write the file. If the latter step fails we will just rewrite to the same file location next time we retry.) - /// Phases 1/2 must be a separate transaction from phase 3, which must be different from phase 4. - /// - /// Phase 1: "prepare" TSMessages for migration, starting with the newest first. - /// This will be enabled at the same time that we enable the BuildFlag to use v2 attachments for - /// _new_ messages, so the start point marks the cutoff between legacy and v2 attachments. - /// We work backwards, newest first, to migrate the legacy attachments. - /// We "prepare" a TSMessage by inserting a row into the TSAttachmentMigration table. - /// - /// Phase 2: I lied in phase 1; _some_ new messages can use legacy attachments even after the cutoff: - /// newly inserted edits on a TSMessage with un-migrated TSAttachments on it. - /// This is somewhat niche, and rare, and migrating oldest-first can result in some buggy behavior in the - /// media gallery, which is why we _mostly_ go backwards (phase 1), but then do a final cleanup by - /// going forwards from the cutoff. Since this only applies to edits made in a narrow window, we mostly - /// expect this phase to no-op and just walk over new messages finding nothing needing migrating. - /// - /// Phase 3: Now that they're prepared, we walk over the TSAttachmentMigration table and migrate - /// the TSAttachments one by one. This is the bulk of the migration. We always work in newest-first order. - /// - /// Phase 4: Delete all the TSAttachment folders and files on disk. Safe to do once phase 3 is complete. - /// - /// When run as a blocking GRDB migration, run the phases in order in back to back, but separate, migrations. - /// This ensures they each get their own transaction, but nothing else can touch the db between them. - /// - /// When run iteratively, we move back and forth between phases 1, 2, and 3. - /// We prepare batches of messages newest first (phase 1) and migrate them (phase 3) until we reach - /// the oldest TSMessage. Then we prepare batches oldest first starting at the cutoff (phase 2) and migrate - /// them (phase 3) until we reach the newest TSMessage. At that point we are done and run phase 4. - public enum TSMessageMigration { - - // MARK: - Phase 1/2 - - /// Phases 1 and 2 when applying as a blocking-on-launch GRDB migration. - static func prepareBlockingTSMessageMigration( - logger: TSAttachmentMigrationLogger, - tx: DBWriteTransaction - ) { - // If we finished phase 2, we are done. - let finished: Bool? = Self.read(key: finishedGoingForwardsKey, tx: tx) - if finished == true { - return - } - - logger.checkpoint() - guard - let maxMigratedRowId: Int64 = Self.read(key: maxMigratedInteractionRowIdKey, tx: tx) - else { - // We've made zero progress. Migrate working backwards from the top (phase 1). - // No need for phase 2, as this will just run top to bottom. - _ = prepareTSMessageMigrationBatch( - batchSize: nil, - maxRowId: nil, - minRowId: nil, - logger: logger, - tx: tx - ) - return - } - - logger.checkpoint() - let finishedGoingBackwards: Bool? = Self.read(key: finishedGoingBackwardsKey, tx: tx) - if finishedGoingBackwards != true { - // We've made partial progress in phase 1, pick up where we left off working backwards. - let minMigratedRowId: Int64? = Self.read(key: minMigratedInteractionRowIdKey, tx: tx) - _ = prepareTSMessageMigrationBatch( - batchSize: nil, - maxRowId: minMigratedRowId ?? maxMigratedRowId, - minRowId: nil, - logger: logger, - tx: tx - ) - } - logger.checkpoint() - - // We finished phase 1. Finish phase 2, picking up wherever we left off. - _ = prepareTSMessageMigrationBatch( - batchSize: nil, - maxRowId: nil, - minRowId: maxMigratedRowId, - logger: logger, - tx: tx - ) - } - - /// Phases 1 and 2 when running as an iterative migration. - /// - Returns - /// True if any rows were migrated; callers should keep calling until it returns false. - public static func prepareNextIterativeTSMessageMigrationBatch( - logger: TSAttachmentMigrationLogger, - tx: DBWriteTransaction - ) -> Bool { - // If we finished phase 2, we are done. - let finished: Bool? = Self.read(key: finishedGoingForwardsKey, tx: tx) - if finished == true { - return false - } - - logger.checkpoint() - let batchSize = 5 - - guard - let maxMigratedRowId: Int64 = Self.read(key: maxMigratedInteractionRowIdKey, tx: tx) - else { - return Self.prepareNextIterativeBatchPhase1ColdStart(batchSize: batchSize, logger: logger, tx: tx) - } - - logger.checkpoint() - // If phase 1 is done, proceed to phase 2. - let finishedGoingBackwards: Bool? = Self.read(key: finishedGoingBackwardsKey, tx: tx) - if finishedGoingBackwards == true { - return Self.prepareNextIteraveBatchPhase2( - batchSize: batchSize, - maxMigratedRowId: maxMigratedRowId, - logger: logger, - tx: tx - ) - } - - logger.checkpoint() - // Otherwise continue our progress on phase 1. - return Self.prepareNextIterativeBatchPhase1( - batchSize: batchSize, - maxMigratedRowId: maxMigratedRowId, - logger: logger, - tx: tx - ) - } - - /// Cold start phase 1; start preparing messages newest-first from the top. - /// - /// - Returns - /// True if any rows were migrated. - private static func prepareNextIterativeBatchPhase1ColdStart( - batchSize: Int, - logger: TSAttachmentMigrationLogger, - tx: DBWriteTransaction - ) -> Bool { - // We've made zero progress. Migrate working backwards from the top (phase 1). - let maxInteractionRowId: Int64? - do { - maxInteractionRowId = try Int64.fetchOne(tx.database, sql: "SELECT max(id) from model_TSInteraction;") - } catch { - owsFail("Failed to read interaction row id") - } - guard let maxInteractionRowId else { - // No interactions. Must be a new install, which is fine, it means we are instantly done. - Self.write(true, key: finishedGoingForwardsKey, tx: tx) - return false - } - logger.checkpoint() - // Write the cutoff point to disk. - Self.write(maxInteractionRowId, key: maxMigratedInteractionRowIdKey, tx: tx) - - // Start going backwards from the top (phase 1). - let lastMigratedRowId = prepareTSMessageMigrationBatch( - batchSize: batchSize, - maxRowId: nil, - minRowId: nil, - logger: logger, - tx: tx - ) - logger.checkpoint() - - if let lastMigratedRowId { - // Save our incremental progress. - Self.write(lastMigratedRowId, key: minMigratedInteractionRowIdKey, tx: tx) - return true - } else { - // If we got nothing back, there were no messages needing migrating. Finish phase 1; - // next batch we try and run will proceed to phase 2. - Self.write(true, key: finishedGoingBackwardsKey, tx: tx) - return true - } - } - - /// - Returns - /// True if any rows were migrated. - private static func prepareNextIterativeBatchPhase1( - batchSize: Int, - maxMigratedRowId: Int64, - logger: TSAttachmentMigrationLogger, - tx: DBWriteTransaction - ) -> Bool { - // Proceed going backwards from the min id, continuing our progress on phase 1. - let minMigratedRowId: Int64? = Self.read(key: minMigratedInteractionRowIdKey, tx: tx) - let lastMigratedId = minMigratedRowId ?? maxMigratedRowId - - let newMinMigratedId = - prepareTSMessageMigrationBatch( - batchSize: batchSize, - maxRowId: lastMigratedId, - minRowId: nil, - logger: logger, - tx: tx - ) - logger.checkpoint() - if let newMinMigratedId { - // Save our incremental progress. - Self.write(newMinMigratedId, key: minMigratedInteractionRowIdKey, tx: tx) - return true - } else { - // If we got nothing back, there were no messages needing migrating. Finish phase 1; - // next batch we try and run will proceed to phase 2. - Self.write(true, key: finishedGoingBackwardsKey, tx: tx) - return true - } - } - - /// - Returns - /// True if any rows were migrated. - private static func prepareNextIteraveBatchPhase2( - batchSize: Int, - maxMigratedRowId: Int64, - logger: TSAttachmentMigrationLogger, - tx: DBWriteTransaction - ) -> Bool { - let newMaxMigratedId = - prepareTSMessageMigrationBatch( - batchSize: batchSize, - maxRowId: nil, - minRowId: maxMigratedRowId, - logger: logger, - tx: tx - ) - logger.checkpoint() - if let newMaxMigratedId { - // Save our incremental progress. - Self.write(newMaxMigratedId, key: maxMigratedInteractionRowIdKey, tx: tx) - return true - } else { - // If we got nothing back, we are finished with phase 2. - // The value of `maxMigratedInteractionRowIdKey` will stay stale, - // but once we write `finishedGoingForwardsKey` it doesn't matter; - // we are done and none of the others get read. - Self.write(true, key: finishedGoingForwardsKey, tx: tx) - return false - } - } - - // MARK: In-progress state - - private static let collectionName = "TSInteraction_TSAttachmentMigration" - // Once true, minMigratedInteractionRowIdKey should be ignored and considered stale; phase 1 is done. - private static let finishedGoingBackwardsKey = "finishedGoingBackwards" - // Once set to true, all other keys are to be ignored and considered stale; phases 1 and 2 are done. - private static let finishedGoingForwardsKey = "finishedGoingForwards" - // Marks how far we got in phase 1, as we work our way backwards (larger to smaller row ids). - private static let minMigratedInteractionRowIdKey = "minMigratedInteractionRowId" - // During phase 1, marks the cutoff where we started migrating. - // During phase 2, marks how far we got as we work our way forwards (smaller to larger row ids). - // Once phase 2 is done (finishedGoingForwardsKey = true) the value is stale and should be ignored. - private static let maxMigratedInteractionRowIdKey = "maxMigratedInteractionRowId" - - private static func read(key: String, tx: DBWriteTransaction) -> T? { - do { - return try T.fetchOne( - tx.database, - sql: "SELECT value from keyvalue WHERE collection = ? AND key = ?", - arguments: [Self.collectionName, key] - ) - } catch { - owsFail("Unable to read key \(key)") - } - } - - private static func write(_ t: T, key: String, tx: DBWriteTransaction) { - do { - try tx.database.execute( - sql: """ - INSERT INTO keyvalue (collection,key,value) VALUES (?,?,?) - ON CONFLICT(key,collection) DO UPDATE SET value = ?; - """, - arguments: [Self.collectionName, key, t, t] - ) - } catch { - owsFail("Unable to write key \(key)") - } - } - - // MARK: - Phase 3 - - /// Phase 3 when applying as a blocking-on-launch GRDB migration. - static func completeBlockingTSMessageMigration(logger: TSAttachmentMigrationLogger, tx: DBWriteTransaction) { - _ = Self.completeTSMessageMigrationBatch(batchSize: nil, logger: logger, tx: tx) - } - - /// Phase 3 when running as an iterative migration. - /// - /// - parameter logger: For logging errors. MUST NOT open a database transaction. - /// - /// - Returns - /// True if any rows were migrated; callers should keep calling until it returns false. - public static func completeNextIterativeTSMessageMigrationBatch( - batchSize: Int = 5, - logger: TSAttachmentMigrationLogger, - tx: DBWriteTransaction - ) -> Bool { - let count = Self.completeTSMessageMigrationBatch(batchSize: batchSize, logger: logger, tx: tx) - return count > 0 - } - - // MARK: - Phase 4 - - /// Phase 4. - /// Works the same whether its run "iteratively" or as a blocking GRDB migration. - public static func cleanUpTSAttachmentFiles() { - // Just try and delete the folder, don't bother checking if we've tried before. - // If the folder is already deleted, this is super cheap. - guard - let rootPath = FileManager.default.containerURL( - forSecurityApplicationGroupIdentifier: TSConstants.applicationGroup - )?.path - else { - return - } - let attachmentsFolder = rootPath.appendingPathComponent("Attachments") - guard OWSFileSystem.deleteFileIfExists(attachmentsFolder) == true else { - owsFailDebug("Unable to delete folder!") - return - } - } - - // MARK: - Migrating batches - - /// Does preparation for another batch, returning the last interaction row id migrated. - /// - /// If a batch size is provided, prepares only that many messages. Otherwise prepares them all. - /// - /// If maxRowId is provided, prepares messages in descending order by row id starting with the provided id (non-inclusive). - /// If minRowId is provided, prepares messages in ascending order by row id starting with the provided id (non-inclusive). - /// If neither is provided, prepares messages in descending order by row id starting with the latest message (inclusive). - private static func prepareTSMessageMigrationBatch( - batchSize: Int?, - maxRowId: Int64?, - minRowId: Int64?, - logger: TSAttachmentMigrationLogger, - tx: DBWriteTransaction - ) -> Int64? { - var sql = "SELECT * FROM model_TSInteraction" - var arguments = StatementArguments() - if let maxRowId { - sql += " WHERE id < ? ORDER BY id DESC;" - _ = arguments.append(contentsOf: [maxRowId]) - } else if let minRowId { - sql += " WHERE id > ? ORDER BY id ASC;" - _ = arguments.append(contentsOf: [minRowId]) - } else { - sql += " ORDER BY id DESC" - } - let cursor: GRDB.RowCursor - do { - cursor = try Row.fetchCursor( - tx.database, - sql: sql, - arguments: arguments - ) - } catch { - logAndFail(logger, error, "Failed to create interaction cursor") - } - func next() -> Row? { - do { - return try cursor.next() - } catch { - logAndFail(logger, error, "Failed to iterate interaction cursor \(error.grdbErrorForLogging)") - } - } - logger.checkpoint() - var batchCount = 0 - var lastMessageRowId: Int64? - while batchCount < batchSize ?? Int.max, let messageRow = next() { - guard let messageRowId = messageRow["id"] as? Int64 else { - logAndFail(logger, nil, "TSInteraction row without id") - } - - guard prepareTSMessageForMigration( - messageRow: messageRow, - messageRowId: messageRowId, - logger: logger, - tx: tx - ) else { - continue - } - logger.checkpoint() - batchCount += 1 - lastMessageRowId = messageRowId - } - return lastMessageRowId - } - - /// Returns true if there was anything to migrate. - fileprivate static func prepareTSMessageForMigration( - messageRow: Row, - messageRowId: Int64, - logger: TSAttachmentMigrationLogger, - tx: DBWriteTransaction - ) -> Bool { - logger.checkpoint() - // Check if the message has any attachments. - let attachmentIds: [String] = ( - bodyAttachmentIds(messageRow: messageRow) - + [ - contactAttachmentId(messageRow: messageRow), - stickerAttachmentId(messageRow: messageRow), - linkPreviewAttachmentId(messageRow: messageRow), - quoteAttachmentId(messageRow: messageRow) - ] - ).compacted() - - guard !attachmentIds.isEmpty else { - return false - } - - for attachmentId in attachmentIds { - var reservedFileIds = TSAttachmentMigration.V1AttachmentReservedFileIds( - tsAttachmentUniqueId: attachmentId, - interactionRowId: messageRowId, - storyMessageRowId: nil, - reservedV2AttachmentPrimaryFileId: UUID(), - reservedV2AttachmentAudioWaveformFileId: UUID(), - reservedV2AttachmentVideoStillFrameFileId: UUID() - ) - do { - logger.checkpoint() - try reservedFileIds.insert(tx.database) - } catch { - logAndFail(logger, error, "Unable to insert reserved file ids") - } - } - return true - } - - private static func logAndFail(_ logger: TSAttachmentMigrationLogger, _ error: Error?, _ logMessage: String) -> Never { - var logMessage = logMessage - if let error = error as? GRDB.DatabaseError, error.resultCode == .SQLITE_CORRUPT { - logMessage = "DB corruption detected! " + logMessage - logger.flagDBCorrupted() - } - logger.didFatalError(logMessage) - owsFail(logMessage) - } - - private static func logAndFailIfDBCorrupted(_ logger: TSAttachmentMigrationLogger, _ error: Error, _ logMessage: String) { - guard let error = error as? GRDB.DatabaseError, error.resultCode == .SQLITE_CORRUPT else { - return - } - let logMessage = "DB corruption detected! " + logMessage - logger.flagDBCorrupted() - logger.didFatalError(logMessage) - owsFail(logMessage) - } - - /// Completes another prepared batch, returns count of touched message rows. - /// - /// If a batch size is provided, prepares only that many prepared messages. Otherwise migares all prepared messages. - private static func completeTSMessageMigrationBatch( - batchSize: Int?, - logger: TSAttachmentMigrationLogger, - tx: DBWriteTransaction - ) -> Int { - logger.checkpoint() - let isRunningIteratively = batchSize != nil - - let reservedFileIdsCursor: RecordCursor - do { - reservedFileIdsCursor = try TSAttachmentMigration.V1AttachmentReservedFileIds - .filter(Column("interactionRowId") != nil) - .order([Column("interactionRowId").desc]) - .fetchCursor(tx.database) - } catch { - logAndFail(logger, error, "Unable to read reserved file ids!") - } - - func nextReservedFileIds() -> TSAttachmentMigration.V1AttachmentReservedFileIds? { - do { - return try reservedFileIdsCursor.next() - } catch { - logAndFail(logger, error, "Unable to read next reserved file ids!") - } - } - logger.checkpoint() - - // row id to (true = migrated) (false = needs re-reservation for next batch) - var migratedMessageRowIds = [Int64: Bool]() - var deletedAttachments = [TSAttachmentMigration.V1Attachment]() - while migratedMessageRowIds.count < batchSize ?? Int.max, let reservedFileIds = nextReservedFileIds() { - autoreleasepool { - guard let messageRowId = reservedFileIds.interactionRowId else { - return - } - if migratedMessageRowIds[messageRowId] != nil { - return - } - - let messageRow: GRDB.Row? - do { - messageRow = try Row.fetchOne( - tx.database, - sql: "SELECT * FROM model_TSInteraction WHERE id = ?;", - arguments: [messageRowId] - ) - } catch { - logAndFail(logger, error, "Failed to fetch interaction row") - } - guard let messageRow else { - logger.checkpoint() - // The message got deleted. Still, count this in the batch - // size so we don't iterate over deleted rows unbounded. - migratedMessageRowIds[messageRowId] = true - reservedFileIds.cleanUpFiles() - return - } - - // We _have_ to migrate everything on a given TSMessage at once. - // Fetch all the reserved ids for the message. - let reservedFileIdsForMessage: [TSAttachmentMigration.V1AttachmentReservedFileIds] - do { - reservedFileIdsForMessage = try TSAttachmentMigration.V1AttachmentReservedFileIds - .filter(Column("interactionRowId") == messageRowId) - .fetchAll(tx.database) - } catch { - logAndFail(logger, error, "Unable to read reserved file ids for message") - } - - let deletedAttachmentsForMessage = Self.migrateMessageAttachments( - reservedFileIds: reservedFileIdsForMessage, - messageRow: messageRow, - messageRowId: messageRowId, - isRunningIteratively: isRunningIteratively, - logger: logger, - tx: tx - ) - logger.checkpoint() - // No need to delete one by one if running non-iteratively; - // we nuke the whole migration table and attachment folder at the end. - if isRunningIteratively { - if let deletedAttachmentsForMessage { - migratedMessageRowIds[messageRowId] = true - deletedAttachments.append(contentsOf: deletedAttachmentsForMessage) - } else { - migratedMessageRowIds[messageRowId] = false - } - } - } - } - - // No need to delete one by one if running non-iteratively; - // we nuke the whole migration table at the end. - if isRunningIteratively { - logger.checkpoint() - // Delete our reserved rows, and re-reserve for those that didn't finish. - for migratedMessageRowId in migratedMessageRowIds { - let didMigrate = migratedMessageRowId.value - let messageRowId = migratedMessageRowId.key - do { - try TSAttachmentMigration.V1AttachmentReservedFileIds - .filter(Column("interactionRowId") == messageRowId) - .deleteAll(tx.database) - } catch { - logAndFail(logger, error, "Unable to delete reserved file ids") - } - - if - !didMigrate, - let messageRow: GRDB.Row = { - do { - return try Row.fetchOne( - tx.database, - sql: "SELECT * FROM model_TSInteraction WHERE id = ?;", - arguments: [messageRowId] - ) - } catch { - logAndFail(logger, error, "Failed to fetch interaction row") - } - }() - { - // Re-reserve new rows; we will migrate in the next batch. - _ = Self.prepareTSMessageForMigration( - messageRow: messageRow, - messageRowId: messageRowId, - logger: logger, - tx: tx - ) - logger.checkpoint() - } - } - tx.addSyncCompletion { - Task { - deletedAttachments.forEach { - logger.checkpoint() - try? $0.deleteFiles() - } - } - } - } - - return migratedMessageRowIds.count - } - - // MARK: - Migrating a single TSMessage - - /// Returns the deleted TSAttachments. - /// Empty array means nothing was migrated but the migration "succeeded" (nothing _needed_ migrating). - /// Nil return value means new attachments were added so we need to re-reserve and migrate again - /// later; reserved Ids should NOT be deleted. - private static func migrateMessageAttachments( - reservedFileIds reservedFileIdsArray: [TSAttachmentMigration.V1AttachmentReservedFileIds], - messageRow: Row, - messageRowId: Int64, - isRunningIteratively: Bool, - logger: TSAttachmentMigrationLogger, - tx: DBWriteTransaction - ) -> [TSAttachmentMigration.V1Attachment]? { - logger.checkpoint() - // From attachment unique id to the reserved file ids. - var reservedFileIdsDict = [String: TSAttachmentMigration.V1AttachmentReservedFileIds]() - for reservedFileIds in reservedFileIdsArray { - reservedFileIdsDict[reservedFileIds.tsAttachmentUniqueId] = reservedFileIds - } - - var bodyTSAttachmentIds = Self.bodyAttachmentIds(messageRow: messageRow) - var messageSticker = Self.messageSticker(messageRow: messageRow) - var stickerTSAttachmentId = messageSticker?.attachmentId - var linkPreview = Self.linkPreview(messageRow: messageRow) - var linkPreviewTSAttachmentId = linkPreview?.imageAttachmentId - var contactShare = Self.contactShare(messageRow: messageRow) - var contactTSAttachmentId = contactShare?.avatarAttachmentId - var quotedMessage = Self.quotedMessage(messageRow: messageRow) - var quotedMessageTSAttachmentId = quotedMessage?.quotedAttachment?.rawAttachmentId.nilIfEmpty - logger.checkpoint() - - if - bodyTSAttachmentIds.isEmpty, - stickerTSAttachmentId == nil, - linkPreviewTSAttachmentId == nil, - contactTSAttachmentId == nil, - quotedMessageTSAttachmentId == nil - { - // This can only happen with state that is malformed somehow, such that - // we couldn't deserialize any of the blob columns. - Logger.info("Attempted to migrate message without attachments.") - // Give up; the message will be marked as migrated and we'll leave - // state as-is. (So whatever invalid state got us here stays invalid). - return [] - } - - var newBodyAttachmentIds: [String]? - var newContact: TSAttachmentMigration.OWSContact? - var newMessageSticker: TSAttachmentMigration.MessageSticker? - var newLinkPreview: TSAttachmentMigration.OWSLinkPreview? - var newQuotedMessage: TSAttachmentMigration.TSQuotedMessage? - - // Remove duplicates. Its unclear _how_ a message ever attained duplicate attachments, - // but it seems it did happen at some point with a bug, so its in people's databases. - var allAttachmentIds = Set() - - // Inserts into the set as well. - func isDuplicate(_ tsAttachmentId: String?) -> Bool { - guard let tsAttachmentId else { - return false - } - // If we inserted, its not a duplicate. - let didInsert = allAttachmentIds.insert(tsAttachmentId).inserted - return !didInsert - } - - // Note this cannot end up as an empty array. If it did, the rest of this method - // would end up broken because we could end up with a content-less message. - bodyTSAttachmentIds = bodyTSAttachmentIds.compactMap { - if isDuplicate($0) { - Logger.warn("Found duplicate body attachment") - return nil - } - return $0 - } - - // Preference order: body > sticker > contact > linkPreview > quote - // Insert each in that order; if its already inserted wipe the var so we pretend - // it never existed. - if isDuplicate(stickerTSAttachmentId) { - Logger.warn("Found duplicate sticker attachment") - newMessageSticker = messageSticker?.removingLegacyAttachment() - messageSticker = nil - stickerTSAttachmentId = nil - } - if isDuplicate(contactTSAttachmentId) { - Logger.warn("Found duplicate contact avatar attachment") - newContact = contactShare?.removingLegacyAttachment() - contactShare = nil - contactTSAttachmentId = nil - } - if isDuplicate(linkPreviewTSAttachmentId) { - Logger.warn("Found duplicate link preview attachment") - newLinkPreview = linkPreview?.removingLegacyAttachment() - linkPreview = nil - linkPreviewTSAttachmentId = nil - } - if isDuplicate(quotedMessageTSAttachmentId) { - Logger.warn("Found duplicate quote attachment") - newQuotedMessage = quotedMessage?.removingLegacyAttachment() - quotedMessage = nil - quotedMessageTSAttachmentId = nil - } - - if allAttachmentIds.isEmpty { - // Nothing to migrate! This can happen if an edit removed attachments. - reservedFileIdsArray.forEach { $0.cleanUpFiles() } - return [] - } - logger.checkpoint() - - // Ensure every attachment is represented in the reserved ids. - let hasUnreservedAttachment = allAttachmentIds.contains(where: { - reservedFileIdsDict[$0] == nil - }) - if hasUnreservedAttachment { - guard isRunningIteratively else { - // If we are running as a blocking GRDB migration this should be impossible. - logAndFail(logger, nil, "Message attachment changed between blocking migrations") - } - reservedFileIdsArray.forEach { $0.cleanUpFiles() } - // Return nil to mark this message and needing another pass. - return nil - } - - guard let threadUniqueId = messageRow["uniqueThreadId"] as? String else { - Logger.error("Missing thread for message") - // Give up; the message will be marked as migrated and we'll leave - // the broken data in the database untouched. - return [] - } - - let threadRowId: Int64? - do { - threadRowId = try Int64.fetchOne( - tx.database, - sql: "SELECT id FROM model_TSThread WHERE uniqueId = ?;", - arguments: [threadUniqueId] - ) - } catch { - logAndFail(logger, error, "Unable to read thread row id") - } - guard let threadRowId else { - Logger.error("Thread doesn't exist for message") - // Give up; the message will be marked as migrated and we'll leave - // the broken data in the database untouched. - return [] - } - - guard - // Row only gives Int64, never UInt64 - let messageReceivedAtTimestampRaw = messageRow["receivedAtTimestamp"] as? Int64 - else { - Logger.error("Missing timestamp for message") - // Give up; the message will be marked as migrated and we'll leave - // the broken data in the database untouched. - return [] - } - logger.checkpoint() - let messageReceivedAtTimestamp = UInt64(bitPattern: messageReceivedAtTimestampRaw) - - let isViewOnce = (messageRow["isViewOnceMessage"] as? Bool) ?? false - let isPastEditRevision = (messageRow["editState"] as? Int) == 2 - - // Edited messages can share attachments with the original. - // Don't delete attachments if this is an edit, just migrate and leave alone. - // We will delete when we get to the original. - let isEditedMessage = messageRow["editState"] as? Int64 == 2 - - var migratedAttachments = [TSAttachmentMigration.V1Attachment]() - func migrateSingleMessageAttachment( - tsAttachmentUniqueId: String, - messageOwnerType: TSAttachmentMigration.V2MessageAttachmentOwnerType, - orderInMessage: Int? = nil, - stickerPackId: Data? = nil, - stickerId: UInt32? = nil - ) { - guard let reservedFileIds = reservedFileIdsDict.removeValue(forKey: tsAttachmentUniqueId) else { - logAndFail(logger, nil, "Missing reservation for attachment") - } - let migratedAttachment = Self.migrateSingleMessageAttachment( - tsAttachmentUniqueId: tsAttachmentUniqueId, - reservedFileIds: reservedFileIds, - messageRowId: messageRowId, - threadRowId: threadRowId, - messageOwnerType: messageOwnerType, - messageReceivedAtTimestamp: messageReceivedAtTimestamp, - isEditedMessage: isEditedMessage, - orderInMessage: orderInMessage.map(UInt32.init(_:)), - stickerPackId: stickerPackId, - stickerId: stickerId, - isViewOnce: isViewOnce, - isPastEditRevision: isPastEditRevision, - logger: logger, - tx: tx - ) - logger.checkpoint() - if let migratedAttachment { - migratedAttachments.append(migratedAttachment) - } - } - - for (index, bodyTSAttachmentId) in bodyTSAttachmentIds.enumerated() { - logger.checkpoint() - migrateSingleMessageAttachment( - tsAttachmentUniqueId: bodyTSAttachmentId, - messageOwnerType: .bodyAttachment, - orderInMessage: index - ) - newBodyAttachmentIds = [] - } - - if let messageSticker, let stickerTSAttachmentId { - logger.checkpoint() - migrateSingleMessageAttachment( - tsAttachmentUniqueId: stickerTSAttachmentId, - messageOwnerType: .sticker, - stickerPackId: messageSticker.info.packId, - stickerId: messageSticker.info.stickerId - ) - newMessageSticker = messageSticker.removingLegacyAttachment() - } - - if let linkPreviewTSAttachmentId { - logger.checkpoint() - migrateSingleMessageAttachment( - tsAttachmentUniqueId: linkPreviewTSAttachmentId, - messageOwnerType: .linkPreview - ) - newLinkPreview = linkPreview?.removingLegacyAttachment() - } - - if let contactTSAttachmentId { - logger.checkpoint() - migrateSingleMessageAttachment( - tsAttachmentUniqueId: contactTSAttachmentId, - messageOwnerType: .contactAvatar - ) - newContact = contactShare?.removingLegacyAttachment() - } - - if - let quotedMessage, - let quotedMessageAttachment = quotedMessage.quotedAttachment, - let quotedMessageTSAttachmentId - { - switch quotedMessageAttachment.attachmentType { - case .thumbnail, .untrustedPointer: - logger.checkpoint() - // Standard case; attachment is wholly owned by this quoted reply - // and no thumbnail-ing is necessary. - migrateSingleMessageAttachment( - tsAttachmentUniqueId: quotedMessageTSAttachmentId, - messageOwnerType: .quotedReplyAttachment - ) - newQuotedMessage = quotedMessage.removingLegacyAttachment() - case .originalForSend, .original: - guard let reservedFileIds = reservedFileIdsDict.removeValue(forKey: quotedMessageTSAttachmentId) else { - logAndFail(logger, nil, "Missing reservation for attachment") - } - logger.checkpoint() - // These point at the attachment of the message being quoted. - // We need to thumbnail the message. - newQuotedMessage = Self.migrateQuotedMessageAttachment( - quotedMessage: quotedMessage, - originalTSAttachmentUniqueId: quotedMessageTSAttachmentId, - reservedFileIds: reservedFileIds, - messageRowId: messageRowId, - threadRowId: threadRowId, - messageReceivedAtTimestamp: messageReceivedAtTimestamp, - isPastEditRevision: isPastEditRevision, - logger: logger, - tx: tx - ) - case .unset, .v2: - // Nothing to migrate - break - } - } - - logger.checkpoint() - Self.updateMessageRow( - rowId: messageRowId, - bodyAttachmentIds: newBodyAttachmentIds, - contact: newContact, - messageSticker: newMessageSticker, - linkPreview: newLinkPreview, - quotedMessage: newQuotedMessage, - logger: logger, - tx: tx - ) - logger.checkpoint() - - // There are two scenarios where the attachment is the _only_ content - // on the message, and if we didn't migrate the message has no content - // and is therefore invalid: - // 1) sticker messages - // 2) body attachment(s) with no body text caption - // All other cases (e.g. link preview) are valid even if the attachment - // gets dropped (e.g. a link preview with no image). - if messageSticker != nil && migratedAttachments.isEmpty { - Logger.error("Failed to migrate sticker; left with invalid content-less message.") - } - if - !bodyTSAttachmentIds.isEmpty, - migratedAttachments.isEmpty, - (messageRow["body"] as? String)?.nilIfEmpty == nil - { - Logger.error("Failed to body attachments without text; left with invalid content-less message.") - } - - return migratedAttachments - } - - // MARK: - Migrating a single TSAttachment - - // Returns the deleted TSAttachment. - private static func migrateSingleMessageAttachment( - tsAttachmentUniqueId: String, - reservedFileIds: TSAttachmentMigration.V1AttachmentReservedFileIds, - messageRowId: Int64, - threadRowId: Int64, - messageOwnerType: TSAttachmentMigration.V2MessageAttachmentOwnerType, - messageReceivedAtTimestamp: UInt64, - isEditedMessage: Bool, - orderInMessage: UInt32?, - stickerPackId: Data?, - stickerId: UInt32?, - isViewOnce: Bool, - isPastEditRevision: Bool, - logger: TSAttachmentMigrationLogger, - tx: DBWriteTransaction - ) -> TSAttachmentMigration.V1Attachment? { - let oldAttachment: TSAttachmentMigration.V1Attachment? - do { - oldAttachment = try TSAttachmentMigration.V1Attachment - .filter(Column("uniqueId") == tsAttachmentUniqueId) - .fetchOne(tx.database) - } catch { - Logger.error("Failed to parse TSAttachment row") - reservedFileIds.cleanUpFiles() - return nil - } - guard let oldAttachment else { - reservedFileIds.cleanUpFiles() - return nil - } - - let attachmentKey: AttachmentKey - if let oldAttachmentKey = try? oldAttachment.encryptionKey.map(AttachmentKey.init(combinedKey:)) { - attachmentKey = oldAttachmentKey - } else { - if oldAttachment.encryptionKey != nil { - Logger.error("TSAttachment has invalid encryption key") - } - attachmentKey = .generate() - } - - let pendingAttachment: TSAttachmentMigration.PendingV2AttachmentFile? - if let oldFilePath = oldAttachment.localFilePath, OWSFileSystem.fileExistsAndIsNotDirectory(atPath: oldFilePath) { - let oldFileUrl = URL(fileURLWithPath: oldFilePath) - do { - logger.checkpoint() - pendingAttachment = try TSAttachmentMigration.V2AttachmentContentValidator.validateContents( - unencryptedFileUrl: oldFileUrl, - reservedFileIds: .init( - primaryFile: reservedFileIds.reservedV2AttachmentPrimaryFileId, - audioWaveform: reservedFileIds.reservedV2AttachmentAudioWaveformFileId, - videoStillFrame: reservedFileIds.reservedV2AttachmentVideoStillFrameFileId - ), - attachmentKey: attachmentKey, - mimeType: oldAttachment.contentType, - renderingFlag: oldAttachment.attachmentType.asRenderingFlag, - sourceFilename: oldAttachment.sourceFilename - ) - } catch _ as TSAttachmentMigration.V2AttachmentContentValidator.AttachmentTooLargeError { - // If we somehow had a file that was too big, just treat it as if we had no file. - pendingAttachment = nil - } catch { - logger.checkpoint() - Logger.error("Failed to validate: \(error). Attempting to copy file and retry") - // If we had a file I/O error (which is the only error thrown), its possible - // it was a transiest file reading permission error that is fixed on device - // restart. Try and work around this by copying the file first to a tmp file, - // then trying again. If this fails give up, dropping the attachment file. - let oldFileUrl = URL(fileURLWithPath: oldFilePath) - let newTmpURL = OWSFileSystem.temporaryFileUrl( - fileName: oldFileUrl.lastPathComponent, - fileExtension: oldFileUrl.pathExtension, - isAvailableWhileDeviceLocked: true - ) - do { - try FileManager.default.copyItem(at: oldFileUrl, to: newTmpURL) - logger.checkpoint() - pendingAttachment = try TSAttachmentMigration.V2AttachmentContentValidator.validateContents( - unencryptedFileUrl: newTmpURL, - reservedFileIds: .init( - primaryFile: reservedFileIds.reservedV2AttachmentPrimaryFileId, - audioWaveform: reservedFileIds.reservedV2AttachmentAudioWaveformFileId, - videoStillFrame: reservedFileIds.reservedV2AttachmentVideoStillFrameFileId - ), - attachmentKey: attachmentKey, - mimeType: oldAttachment.contentType, - renderingFlag: oldAttachment.attachmentType.asRenderingFlag, - sourceFilename: oldAttachment.sourceFilename - ) - Logger.info("Succesfully validated after copying file") - } catch { - Logger.error("File i/o failure of copied file: \(error.grdbErrorForLogging)") - pendingAttachment = nil - } - } - } else { - logger.checkpoint() - // A pointer; no validation needed. - pendingAttachment = nil - // Clean up files just in case. - reservedFileIds.cleanUpFiles() - } - - let v2AttachmentId: Int64 - if - let pendingAttachment, - let existingV2Attachment = { - do { - return try TSAttachmentMigration.V2Attachment - .filter(Column("sha256ContentHash") == pendingAttachment.sha256ContentHash) - .fetchOne(tx.database) - } catch { - logAndFail(logger, error, "Failed to fetch v2 attachment") - } - }() - { - logger.checkpoint() - // If we already have a v2 attachment with the same plaintext hash, - // create new references to it and drop the pending attachment. - v2AttachmentId = existingV2Attachment.id! - // Delete the reserved files being used by the pending attachment. - reservedFileIds.cleanUpFiles() - } else { - var v2Attachment: TSAttachmentMigration.V2Attachment - if let pendingAttachment { - v2Attachment = TSAttachmentMigration.V2Attachment( - blurHash: pendingAttachment.blurHash, - sha256ContentHash: pendingAttachment.sha256ContentHash, - encryptedByteCount: pendingAttachment.encryptedByteCount, - unencryptedByteCount: pendingAttachment.unencryptedByteCount, - mimeType: pendingAttachment.mimeType, - encryptionKey: pendingAttachment.encryptionKey, - digestSHA256Ciphertext: pendingAttachment.digestSHA256Ciphertext, - contentType: UInt32(pendingAttachment.validatedContentType.rawValue), - transitCdnNumber: oldAttachment.cdnNumber, - transitCdnKey: oldAttachment.cdnKey, - transitUploadTimestamp: oldAttachment.uploadTimestamp, - transitEncryptionKey: attachmentKey.combinedKey, - transitUnencryptedByteCount: pendingAttachment.unencryptedByteCount, - transitDigestSHA256Ciphertext: oldAttachment.digest, - lastTransitDownloadAttemptTimestamp: nil, - localRelativeFilePath: pendingAttachment.localRelativeFilePath, - cachedAudioDurationSeconds: pendingAttachment.audioDurationSeconds, - cachedMediaHeightPixels: pendingAttachment.mediaSizePixels.map { UInt32($0.height) }, - cachedMediaWidthPixels: pendingAttachment.mediaSizePixels.map { UInt32($0.width) }, - cachedVideoDurationSeconds: pendingAttachment.videoDurationSeconds, - audioWaveformRelativeFilePath: pendingAttachment.audioWaveformRelativeFilePath, - videoStillFrameRelativeFilePath: pendingAttachment.videoStillFrameRelativeFilePath - ) - } else { - v2Attachment = TSAttachmentMigration.V2Attachment( - blurHash: oldAttachment.blurHash, - sha256ContentHash: nil, - encryptedByteCount: nil, - unencryptedByteCount: nil, - mimeType: oldAttachment.contentType, - encryptionKey: attachmentKey.combinedKey, - digestSHA256Ciphertext: nil, - contentType: nil, - transitCdnNumber: oldAttachment.cdnNumber, - transitCdnKey: oldAttachment.cdnKey, - transitUploadTimestamp: oldAttachment.uploadTimestamp, - transitEncryptionKey: attachmentKey.combinedKey, - transitUnencryptedByteCount: oldAttachment.byteCount, - transitDigestSHA256Ciphertext: oldAttachment.digest, - lastTransitDownloadAttemptTimestamp: nil, - localRelativeFilePath: nil, - cachedAudioDurationSeconds: nil, - cachedMediaHeightPixels: nil, - cachedMediaWidthPixels: nil, - cachedVideoDurationSeconds: nil, - audioWaveformRelativeFilePath: nil, - videoStillFrameRelativeFilePath: nil - ) - } - - do { - try v2Attachment.insert(tx.database) - } catch { - // If db corruption, flag that and exit. - // Otherwise just drop this attachment and keep going. - logAndFailIfDBCorrupted(logger, error, "Failed to insert v2 attachment \(error)") - cleanUpTSAttachmentFiles() - return nil - } - v2AttachmentId = v2Attachment.id! - } - logger.checkpoint() - - let ownerTypeRaw: UInt32 - switch messageOwnerType { - case .bodyAttachment: - // Oversize text is a "body attachment" in v1, but a separate type - // in v2. If this is the first attachment and it matches the oversize - // text MIME type, re-map it to oversize text. - if orderInMessage == 0, pendingAttachment?.mimeType == "text/x-signal-plain" { - ownerTypeRaw = UInt32(TSAttachmentMigration.V2MessageAttachmentOwnerType.oversizeText.rawValue) - } else { - // Uniquely, non-oversize-text body attachments are present in the - // media gallery table and need to be deleted from there. - try? oldAttachment.deleteMediaGalleryRecord(tx: tx) - fallthrough - } - default: - ownerTypeRaw = UInt32(messageOwnerType.rawValue) - } - - let (sourceMediaHeightPixels, sourceMediaWidthPixels) = (try? oldAttachment.sourceMediaSizePixels()) ?? (nil, nil) - - let reference = TSAttachmentMigration.MessageAttachmentReference( - ownerType: ownerTypeRaw, - ownerRowId: messageRowId, - attachmentRowId: v2AttachmentId, - receivedAtTimestamp: messageReceivedAtTimestamp, - contentType: pendingAttachment.map { UInt32($0.validatedContentType.rawValue) }, - renderingFlag: UInt32(oldAttachment.attachmentType.asRenderingFlag.rawValue), - idInMessage: oldAttachment.clientUuid, - orderInMessage: orderInMessage, - threadRowId: threadRowId, - caption: oldAttachment.caption, - sourceFilename: oldAttachment.sourceFilename, - sourceUnencryptedByteCount: oldAttachment.byteCount, - sourceMediaHeightPixels: sourceMediaHeightPixels, - sourceMediaWidthPixels: sourceMediaWidthPixels, - stickerPackId: stickerPackId, - stickerId: stickerId, - isViewOnce: isViewOnce, - ownerIsPastEditRevision: isPastEditRevision - ) - do { - try reference.insert(tx.database) - } catch { - logAndFail(logger, error, "Failed to insert attachment reference") - } - logger.checkpoint() - - // Edits might be reusing the original's TSAttachment. - // DON'T delete the TSAttachment so its still available for the original. - // Also don't return it (so we don't delete its files either). - // If it turns out the original doesn't reuse (e.g. we edited oversize text), - // this attachment will stick around until the migration is done, but - // will get deleted when we bulk delete the table and folder at the end. - if isEditedMessage { - return nil - } - - do { - try oldAttachment.delete(tx.database) - } catch { - // If db corruption, flag that and exit. - // Otherwise just drop this attachment and keep going. - logAndFailIfDBCorrupted(logger, error, "Failed to delete v2 attachment \(error.grdbErrorForLogging)") - logger.checkpoint() - cleanUpTSAttachmentFiles() - return nil - } - - return oldAttachment - } - - /// Given the unique id of the _original_ message's attachment and a reply message's row id, - /// thumbnails the attachment if possible and assigns the thumbnail to the provided message row id. - /// - /// Returns the new TSQuotedMessage to use on the reply TSMessage. - /// DOES NOT delete the original attachment. - private static func migrateQuotedMessageAttachment( - quotedMessage: TSQuotedMessage, - originalTSAttachmentUniqueId: String, - reservedFileIds: TSAttachmentMigration.V1AttachmentReservedFileIds, - messageRowId: Int64, - threadRowId: Int64, - messageReceivedAtTimestamp: UInt64, - isPastEditRevision: Bool, - logger: TSAttachmentMigrationLogger, - tx: DBWriteTransaction - ) -> TSAttachmentMigration.TSQuotedMessage { - let oldAttachment: TSAttachmentMigration.V1Attachment? - do { - oldAttachment = try TSAttachmentMigration.V1Attachment - .filter(Column("uniqueId") == originalTSAttachmentUniqueId) - .fetchOne(tx.database) - } catch { - logger.checkpoint() - Logger.error("Failed to parse quote TSAttachment") - // We can easily fall back to stub, just drop the attachment. - reservedFileIds.cleanUpFiles() - return quotedMessage.fallbackToStub() - } - - guard let oldAttachment else { - logger.checkpoint() - // We've got no original attachment at all. - // This can happen if the quote came in, then the original got deleted - // while the quote still pointed at the original's attachment. - // Just fall back to a stub. - reservedFileIds.cleanUpFiles() - return quotedMessage.fallbackToStub() - } - - let rawContentType = TSAttachmentMigration.V2AttachmentContentValidator.rawContentType( - mimeType: oldAttachment.contentType - ) - - guard - let oldFilePath = oldAttachment.localFilePath, - OWSFileSystem.fileExistsAndIsNotDirectory(atPath: oldFilePath), - rawContentType == .image || rawContentType == .video || rawContentType == .animatedImage - else { - logger.checkpoint() - // We've got no original media stream, just a pointer or non-visual media. - // We can't easily handle this, so instead just fall back to a stub. - reservedFileIds.cleanUpFiles() - return quotedMessage.fallbackToStub(oldAttachment) - } - - let pendingAttachment: TSAttachmentMigration.PendingV2AttachmentFile? - do { - logger.checkpoint() - pendingAttachment = try TSAttachmentMigration.V2AttachmentContentValidator.prepareQuotedReplyThumbnail( - fromOriginalAttachmentStream: oldAttachment, - reservedFileIds: .init( - primaryFile: reservedFileIds.reservedV2AttachmentPrimaryFileId, - audioWaveform: reservedFileIds.reservedV2AttachmentAudioWaveformFileId, - videoStillFrame: reservedFileIds.reservedV2AttachmentVideoStillFrameFileId - ), - renderingFlag: oldAttachment.attachmentType.asRenderingFlag, - sourceFilename: oldAttachment.sourceFilename - ) - } catch { - Logger.error("Error validating quote attachment") - pendingAttachment = nil - } - guard let pendingAttachment else { - logger.checkpoint() - Logger.error("Failed to validate quote attachment") - reservedFileIds.cleanUpFiles() - return quotedMessage.fallbackToStub(oldAttachment) - } - - let v2AttachmentId: Int64 - - let existingV2Attachment: TSAttachmentMigration.V2Attachment? - do { - existingV2Attachment = try TSAttachmentMigration.V2Attachment - .filter(Column("sha256ContentHash") == pendingAttachment.sha256ContentHash) - .fetchOne(tx.database) - } catch { - logAndFail(logger, error, "Failed to fetch v2 attachment") - } - if let existingV2Attachment { - logger.checkpoint() - // If we already have a v2 attachment with the same plaintext hash, - // create new references to it and drop the pending attachment. - v2AttachmentId = existingV2Attachment.id! - // Delete the reserved files being used by the pending attachment. - reservedFileIds.cleanUpFiles() - } else { - var v2Attachment = TSAttachmentMigration.V2Attachment( - blurHash: pendingAttachment.blurHash, - sha256ContentHash: pendingAttachment.sha256ContentHash, - encryptedByteCount: pendingAttachment.encryptedByteCount, - unencryptedByteCount: pendingAttachment.unencryptedByteCount, - mimeType: pendingAttachment.mimeType, - encryptionKey: pendingAttachment.encryptionKey, - digestSHA256Ciphertext: pendingAttachment.digestSHA256Ciphertext, - contentType: UInt32(pendingAttachment.validatedContentType.rawValue), - transitCdnNumber: nil, - transitCdnKey: nil, - transitUploadTimestamp: nil, - transitEncryptionKey: nil, - transitUnencryptedByteCount: nil, - transitDigestSHA256Ciphertext: nil, - lastTransitDownloadAttemptTimestamp: nil, - localRelativeFilePath: pendingAttachment.localRelativeFilePath, - cachedAudioDurationSeconds: pendingAttachment.audioDurationSeconds, - cachedMediaHeightPixels: pendingAttachment.mediaSizePixels.map { UInt32($0.height) }, - cachedMediaWidthPixels: pendingAttachment.mediaSizePixels.map { UInt32($0.width) }, - cachedVideoDurationSeconds: pendingAttachment.videoDurationSeconds, - audioWaveformRelativeFilePath: pendingAttachment.audioWaveformRelativeFilePath, - videoStillFrameRelativeFilePath: pendingAttachment.videoStillFrameRelativeFilePath - ) - - do { - try v2Attachment.insert(tx.database) - } catch { - // If db corruption, flag that and exit. - // Otherwise just drop this attachment and keep going. - logAndFailIfDBCorrupted(logger, error, "Failed to insert v2 attachment \(error)") - logger.checkpoint() - cleanUpTSAttachmentFiles() - return quotedMessage.fallbackToStub(oldAttachment) - } - v2AttachmentId = v2Attachment.id! - } - - let reference = TSAttachmentMigration.MessageAttachmentReference( - ownerType: UInt32(TSAttachmentMigration.V2MessageAttachmentOwnerType.quotedReplyAttachment.rawValue), - ownerRowId: messageRowId, - attachmentRowId: v2AttachmentId, - receivedAtTimestamp: messageReceivedAtTimestamp, - contentType: UInt32(pendingAttachment.validatedContentType.rawValue), - renderingFlag: UInt32(pendingAttachment.renderingFlag.rawValue), - idInMessage: nil, - orderInMessage: nil, - threadRowId: threadRowId, - caption: nil, - sourceFilename: pendingAttachment.sourceFilename, - sourceUnencryptedByteCount: nil, - sourceMediaHeightPixels: nil, - sourceMediaWidthPixels: nil, - stickerPackId: nil, - stickerId: nil, - // Quoted message attachments cannot be view once - isViewOnce: false, - ownerIsPastEditRevision: isPastEditRevision - ) - do { - try reference.insert(tx.database) - } catch { - logAndFail(logger, error, "Failed to insert attachment reference") - } - logger.checkpoint() - - // NOTE: we DO NOT delete the old attachment. It belongs to the original message. - - let newQuotedMessage = quotedMessage - let newQuotedAttachment = newQuotedMessage.quotedAttachment - newQuotedAttachment?.attachmentType = .v2 - newQuotedAttachment?.rawAttachmentId = "" - newQuotedAttachment?.contentType = nil - newQuotedAttachment?.sourceFilename = nil - newQuotedMessage.quotedAttachment = newQuotedAttachment - return newQuotedMessage - } - - // MARK: - NSKeyedArchiver/Unarchiver - - private static func unarchive(_ data: Data) throws -> T { - let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data) - unarchiver.requiresSecureCoding = false - TSAttachmentMigration.prepareNSCodingMappings(unarchiver: unarchiver) - let decoded = try unarchiver.decodeTopLevelObject(of: [T.self], forKey: NSKeyedArchiveRootObjectKey) - guard let decoded = decoded as? T else { - throw OWSAssertionError("Expected \(T.self) but decoded \(type(of: decoded))") - } - return decoded - } - - private static func bodyAttachmentIds(messageRow: Row) -> [String] { - guard let encoded = messageRow["deprecated_attachmentIds"] as? Data else { - return [] - } - do { - let decoded: NSArray = try unarchive(encoded) - - var array = [String]() - try decoded.forEach { element in - guard let attachmentId = element as? String else { - throw OWSAssertionError("Invalid attachment id") - } - array.append(attachmentId) - } - return array - } catch { - Logger.error("Failed to unarchive body attachments") - return [] - } - } - - private static func contactShare(messageRow: Row) -> TSAttachmentMigration.OWSContact? { - guard let encoded = messageRow["contactShare"] as? Data else { - return nil - } - do { - return try unarchive(encoded) - } catch { - Logger.error("Failed to unarchive contact share") - return nil - } - } - - private static func contactAttachmentId(messageRow: Row) -> String? { - return contactShare(messageRow: messageRow)?.avatarAttachmentId - } - - private static func messageSticker(messageRow: Row) -> TSAttachmentMigration.MessageSticker? { - guard let encoded = messageRow["messageSticker"] as? Data else { - return nil - } - do { - return try unarchive(encoded) - } catch { - Logger.error("Failed to unarchive sticker") - return nil - } - } - - private static func stickerAttachmentId(messageRow: Row) -> String? { - return messageSticker(messageRow: messageRow)?.attachmentId - } - - private static func linkPreview(messageRow: Row) -> TSAttachmentMigration.OWSLinkPreview? { - guard let encoded = messageRow["linkPreview"] as? Data else { - return nil - } - do { - return try unarchive(encoded) - } catch { - Logger.error("Failed to unarchive link preview") - return nil - } - } - - private static func linkPreviewAttachmentId(messageRow: Row) -> String? { - return linkPreview(messageRow: messageRow)?.imageAttachmentId - } - - private static func quotedMessage(messageRow: Row) -> TSAttachmentMigration.TSQuotedMessage? { - guard let encoded = messageRow["quotedMessage"] as? Data else { - return nil - } - do { - return try unarchive(encoded) - } catch { - Logger.error("Failed to unarchive quoted message") - return nil - } - } - - private static func quoteAttachmentId(messageRow: Row) -> String? { - return quotedMessage(messageRow: messageRow)?.quotedAttachment?.rawAttachmentId.nilIfEmpty - } - - private static func archive(_ value: Any) -> Data { - let archiver = NSKeyedArchiver(requiringSecureCoding: false) - TSAttachmentMigration.prepareNSCodingMappings(archiver: archiver) - archiver.encode(value, forKey: NSKeyedArchiveRootObjectKey) - return archiver.encodedData - } - - private static func updateMessageRow( - rowId: Int64, - bodyAttachmentIds: [String]?, - contact: TSAttachmentMigration.OWSContact?, - messageSticker: TSAttachmentMigration.MessageSticker?, - linkPreview: TSAttachmentMigration.OWSLinkPreview?, - quotedMessage: TSAttachmentMigration.TSQuotedMessage?, - logger: TSAttachmentMigrationLogger, - tx: DBWriteTransaction - ) { - var sql = "UPDATE model_TSInteraction SET " - var arguments = StatementArguments() - - var columns = [String]() - if let bodyAttachmentIds { - columns.append("deprecated_attachmentIds") - _ = arguments.append(contentsOf: [archive(bodyAttachmentIds)]) - } - if let contact { - columns.append("contactShare") - _ = arguments.append(contentsOf: [archive(contact)]) - } - if let messageSticker { - columns.append("messageSticker") - _ = arguments.append(contentsOf: [archive(messageSticker)]) - } - if let linkPreview { - columns.append("linkPreview") - _ = arguments.append(contentsOf: [archive(linkPreview)]) - } - if let quotedMessage { - columns.append("quotedMessage") - _ = arguments.append(contentsOf: [archive(quotedMessage)]) - } - - sql.append(columns.map({ $0 + " = ?"}).joined(separator: ", ")) - sql.append(" WHERE id = ?;") - _ = arguments.append(contentsOf: [rowId]) - do { - try tx.database.execute(sql: sql, arguments: arguments) - } catch { - // Everything we would write to the db would be to clear out deprecated - // fields that are no longer read. If it fails, ignore it (unless db corruption). - logAndFailIfDBCorrupted(logger, error, "Failed to update message columns: \(columns)") - } - } - } -} - -extension TSAttachmentMigration.MessageSticker { - - fileprivate func removingLegacyAttachment() -> Self { - attachmentId = nil - return self - } -} - -extension TSAttachmentMigration.OWSContact { - - fileprivate func removingLegacyAttachment() -> Self { - avatarAttachmentId = nil - return self - } -} - -extension TSAttachmentMigration.OWSLinkPreview { - - fileprivate func removingLegacyAttachment() -> Self { - imageAttachmentId = nil - usesV2AttachmentReferenceValue = NSNumber(value: true) - return self - } -} - -extension TSAttachmentMigration.TSQuotedMessage { - - fileprivate func removingLegacyAttachment() -> Self { - let newQuotedAttachment = quotedAttachment - newQuotedAttachment?.rawAttachmentId = "" - newQuotedAttachment?.attachmentType = .v2 - newQuotedAttachment?.contentType = nil - newQuotedAttachment?.sourceFilename = nil - self.quotedAttachment = newQuotedAttachment - return self - } - - fileprivate func fallbackToStub( - _ oldAttachment: TSAttachmentMigration.V1Attachment? = nil - ) -> Self { - let newQuotedAttachment = self.quotedAttachment - newQuotedAttachment?.attachmentType = .unset - newQuotedAttachment?.rawAttachmentId = "" - if let oldAttachment { - newQuotedAttachment?.contentType = oldAttachment.contentType - newQuotedAttachment?.sourceFilename = oldAttachment.sourceFilename - } - self.quotedAttachment = newQuotedAttachment - return self - } -} diff --git a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+ThreadWallpaper.swift b/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+ThreadWallpaper.swift deleted file mode 100644 index 8f834cf2d4..0000000000 --- a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration+ThreadWallpaper.swift +++ /dev/null @@ -1,239 +0,0 @@ -// -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -import Foundation -import GRDB - -/// Migration for thread wallpapers, which were previously represented as an unencrypted -/// file on disk in a particular folder with the thread unique id as the file name. -/// After migrating they are full-fledged v2 attachments in the ThreadAttachmentReference table. -/// -/// The migration works in two passes which must happen in separate write transactions but -/// must be run back to back. (Why? Filesystem changes are not part of the db transaction, -/// so we need to first "reserve" the final file location in the db and then write the file. If the latter -/// step fails we will just rewrite to the same file location next time we retry.) -/// -/// Phase 1: Read the key value store for threads with image wallpapers, for each one create -/// a _new_ key value store entry with the "reserved" (random) final file location. -/// -/// Phase 2: for each reserved file location, do image validation and create the v2 attachments. -/// Delete the originals. -extension TSAttachmentMigration { - - fileprivate static let legacyWallpaperDirectory = URL( - fileURLWithPath: "Wallpapers", - isDirectory: true, - relativeTo: FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: TSConstants.applicationGroup)! - ) - - /// First pass - static func prepareThreadWallpaperMigration(tx: DBWriteTransaction) throws { - let rows = try Row.fetchAll( - tx.database, - sql: "SELECT * FROM keyvalue WHERE collection = ?", - arguments: ["Wallpaper+Enum"] - ) - for row in rows { - guard - let valueData = row["value"] as? Data, - let valueDecoded = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSString.self, from: valueData), - valueDecoded == "photo", - // Actually either thread unique id or "global"; - // we replicate that in our new entry. - let threadUniqueId = row["key"] as? String - else { - continue - } - let reservedFileUUID = UUID().uuidString - // Reinsert with the new collection name. - try tx.database.execute( - sql: """ - INSERT INTO keyvalue - (collection, key, value) - VALUES (?, ?, ?) - """, - arguments: ["WallpaperMigration", threadUniqueId, reservedFileUUID] - ) - } - } - - /// Second pass - static func completeThreadWallpaperMigration(tx: DBWriteTransaction) throws { - let reservedFileRows = try Row.fetchAll( - tx.database, - sql: "SELECT * FROM keyvalue WHERE collection = ?", - arguments: ["WallpaperMigration"] - ) - for row in reservedFileRows { - guard - let reservedFileUUIDRaw = row["value"] as? String, - let reservedFileUUID = UUID(uuidString: reservedFileUUIDRaw), - let threadUniqueIdOrGlobal = row["key"] as? String - else { - continue - } - // Nil for the global thread wallpaper. - let threadRowId: Int64? - let wallpaperFilename: String - if threadUniqueIdOrGlobal == "global" { - threadRowId = nil - wallpaperFilename = "global" - } else { - guard - let fetchedThreadRowId = try Int64.fetchOne( - tx.database, - sql: "SELECT id FROM model_TSThread WHERE uniqueId = ?", - arguments: [threadUniqueIdOrGlobal] - ), - let filename = threadUniqueIdOrGlobal.addingPercentEncoding(withAllowedCharacters: .alphanumerics) - else { - continue - } - threadRowId = fetchedThreadRowId - wallpaperFilename = filename - } - - do { - try Self.migrateSingleWallpaper( - reservedFileUUID: reservedFileUUID, - threadRowId: threadRowId, - wallpaperFilename: wallpaperFilename, - tx: tx - ) - } catch { - // Ignore failues on individual wallpapers; we'll just drop them if they're unable - // to get migrated. But delete at the reserved file location just in case. - try OWSFileSystem.deleteFileIfExists( - url: TSAttachmentMigration.V2Attachment.absoluteAttachmentFileURL( - relativeFilePath: TSAttachmentMigration.V2Attachment.relativeFilePath(reservedUUID: reservedFileUUID) - ) - ) - } - } - - // Delete our reserved rows. - try tx.database.execute( - sql: "DELETE FROM keyvalue where collection = ?", - arguments: ["WallpaperMigration"] - ) - } - - // Delete the old wallpaper directory. - static func cleanUpLegacyThreadWallpaperDirectory() throws { - for filePath in try OWSFileSystem.recursiveFilesInDirectory(legacyWallpaperDirectory.path) { - if !OWSFileSystem.deleteFile(filePath, ignoreIfMissing: true) { - throw OWSAssertionError("Failed to delete!") - } - } - } - - private static func migrateSingleWallpaper( - reservedFileUUID: UUID, - threadRowId: Int64?, - wallpaperFilename: String, - tx: DBWriteTransaction - ) throws { - let wallpaperFile = URL( - fileURLWithPath: wallpaperFilename, - isDirectory: false, - relativeTo: legacyWallpaperDirectory - ) - - let reservedFileIds = TSAttachmentMigration.V2AttachmentContentValidator.ReservedRelativeFileIds( - primaryFile: reservedFileUUID, - audioWaveform: UUID() /* unused */, - videoStillFrame: UUID() /* unused */ - ) - - let pendingAttachment: TSAttachmentMigration.PendingV2AttachmentFile - do { - pendingAttachment = try TSAttachmentMigration.V2AttachmentContentValidator.validateContents( - unencryptedFileUrl: wallpaperFile, - reservedFileIds: reservedFileIds, - attachmentKey: nil, - mimeType: "image/jpeg", - renderingFlag: .default, - sourceFilename: nil - ) - } catch { - guard - let imageData = try? Data(contentsOf: wallpaperFile), - let image = UIImage(data: imageData), - let resizedImage = image.resized(maxDimensionPixels: CGFloat(TSAttachmentMigration.kMaxStillImageDimensions)), - let resizedImageData = resizedImage.jpegData(compressionQuality: 0.8) - else { - // We can't get the image to resize. - // Just drop it; the wallpaper will be lost. - return - } - - // Try again with a resized image. - let resizedImageFile = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true) - try resizedImageData.write(to: resizedImageFile) - pendingAttachment = try TSAttachmentMigration.V2AttachmentContentValidator.validateContents( - unencryptedFileUrl: resizedImageFile, - reservedFileIds: reservedFileIds, - attachmentKey: nil, - mimeType: "image/jpeg", - renderingFlag: .default, - sourceFilename: nil - ) - } - - let v2AttachmentId: Int64 - if - let existingV2Attachment = try TSAttachmentMigration.V2Attachment - .filter(Column("sha256ContentHash") == pendingAttachment.sha256ContentHash) - .fetchOne(tx.database) - { - // If we already have a v2 attachment with the same plaintext hash, - // create new references to it and drop the pending attachment. - v2AttachmentId = existingV2Attachment.id! - // Delete the reserved files being used by the pending attachment. - try OWSFileSystem.deleteFileIfExists( - url: TSAttachmentMigration.V2Attachment.absoluteAttachmentFileURL( - relativeFilePath: TSAttachmentMigration.V2Attachment.relativeFilePath( - reservedUUID: reservedFileUUID - ) - ) - ) - } else { - var v2Attachment = TSAttachmentMigration.V2Attachment( - blurHash: pendingAttachment.blurHash, - sha256ContentHash: pendingAttachment.sha256ContentHash, - encryptedByteCount: pendingAttachment.encryptedByteCount, - unencryptedByteCount: pendingAttachment.unencryptedByteCount, - mimeType: pendingAttachment.mimeType, - encryptionKey: pendingAttachment.encryptionKey, - digestSHA256Ciphertext: pendingAttachment.digestSHA256Ciphertext, - contentType: UInt32(pendingAttachment.validatedContentType.rawValue), - transitCdnNumber: nil, - transitCdnKey: nil, - transitUploadTimestamp: nil, - transitEncryptionKey: nil, - transitUnencryptedByteCount: nil, - transitDigestSHA256Ciphertext: nil, - lastTransitDownloadAttemptTimestamp: nil, - localRelativeFilePath: pendingAttachment.localRelativeFilePath, - cachedAudioDurationSeconds: pendingAttachment.audioDurationSeconds, - cachedMediaHeightPixels: pendingAttachment.mediaSizePixels.map { UInt32($0.height) }, - cachedMediaWidthPixels: pendingAttachment.mediaSizePixels.map { UInt32($0.width) }, - cachedVideoDurationSeconds: pendingAttachment.videoDurationSeconds, - audioWaveformRelativeFilePath: pendingAttachment.audioWaveformRelativeFilePath, - videoStillFrameRelativeFilePath: pendingAttachment.videoStillFrameRelativeFilePath - ) - - try v2Attachment.insert(tx.database) - v2AttachmentId = v2Attachment.id! - } - - let reference = TSAttachmentMigration.ThreadAttachmentReference( - ownerRowId: threadRowId, - attachmentRowId: v2AttachmentId, - creationTimestamp: Date().ows_millisecondsSince1970 - ) - try reference.insert(tx.database) - } -} diff --git a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration.swift b/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration.swift deleted file mode 100644 index 4b170a0256..0000000000 --- a/SignalServiceKit/Storage/Database/IncrementalMigrations/TSAttachment/TSAttachmentMigration.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -import Foundation -import GRDB - -/// Just a namespace for TSAttachment migration related types. -public enum TSAttachmentMigration {} diff --git a/SignalServiceKit/TestUtils/MockSSKEnvironment.swift b/SignalServiceKit/TestUtils/MockSSKEnvironment.swift index cc6f8a7a02..04c52302ef 100644 --- a/SignalServiceKit/TestUtils/MockSSKEnvironment.swift +++ b/SignalServiceKit/TestUtils/MockSSKEnvironment.swift @@ -16,7 +16,6 @@ public class MockSSKEnvironment { callMessageHandler: any CallMessageHandler = NoopCallMessageHandler(), currentCallProvider: any CurrentCallProvider = CurrentCallNoOpProvider(), notificationPresenter: any NotificationPresenter = NoopNotificationPresenterImpl(), - incrementalMessageTSAttachmentMigratorFactory: any IncrementalMessageTSAttachmentMigratorFactory = IncrementalMessageTSAttachmentMigratorFactoryMock(), testDependencies: AppSetup.TestDependencies? = nil ) async { owsPrecondition(!(CurrentAppContext() is TestAppContext)) @@ -51,7 +50,6 @@ public class MockSSKEnvironment { callMessageHandler: callMessageHandler, currentCallProvider: currentCallProvider, notificationPresenter: notificationPresenter, - incrementalMessageTSAttachmentMigratorFactory: incrementalMessageTSAttachmentMigratorFactory, testDependencies: testDependencies ?? AppSetup.TestDependencies( contactManager: FakeContactsManager(), groupV2Updates: MockGroupV2Updates(), diff --git a/SignalServiceKit/tests/MessageBackup/BackupArchiveIntegrationTests.swift b/SignalServiceKit/tests/MessageBackup/BackupArchiveIntegrationTests.swift index fbc69adae7..c24abc7b63 100644 --- a/SignalServiceKit/tests/MessageBackup/BackupArchiveIntegrationTests.swift +++ b/SignalServiceKit/tests/MessageBackup/BackupArchiveIntegrationTests.swift @@ -332,7 +332,6 @@ class BackupArchiveIntegrationTests: XCTestCase { callMessageHandler: CrashyMocks.MockCallMessageHandler(), currentCallProvider: CrashyMocks.MockCurrentCallThreadProvider(), notificationPresenter: CrashyMocks.MockNotificationPresenter(), - incrementalMessageTSAttachmentMigratorFactory: NoOpIncrementalMessageTSAttachmentMigratorFactory(), testDependencies: AppSetup.TestDependencies( backupAttachmentCoordinator: MockBackupAttachmentCoordinator(), dateProvider: dateProvider, diff --git a/SignalShareExtension/ShareViewController.swift b/SignalShareExtension/ShareViewController.swift index 5ce62f8e27..8feaaad2f8 100644 --- a/SignalShareExtension/ShareViewController.swift +++ b/SignalShareExtension/ShareViewController.swift @@ -98,7 +98,6 @@ public class ShareViewController: OWSNavigationController, ShareViewDelegate, SA callMessageHandler: NoopCallMessageHandler(), currentCallProvider: CurrentCallNoOpProvider(), notificationPresenter: NoopNotificationPresenterImpl(), - incrementalMessageTSAttachmentMigratorFactory: NoOpIncrementalMessageTSAttachmentMigratorFactory(), ) // Configure the rest of the globals before preparing the database. diff --git a/SignalUI/Attachments/PreviewableAttachment.swift b/SignalUI/Attachments/PreviewableAttachment.swift index 8832431393..302e117e37 100644 --- a/SignalUI/Attachments/PreviewableAttachment.swift +++ b/SignalUI/Attachments/PreviewableAttachment.swift @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // -import Foundation +public import AVFoundation public import SignalServiceKit /// Represents an attachment the user *might* choose to send. diff --git a/SignalUI/Attachments/SendableAttachment.swift b/SignalUI/Attachments/SendableAttachment.swift index 4f9f3fe50c..6aabe1c709 100644 --- a/SignalUI/Attachments/SendableAttachment.swift +++ b/SignalUI/Attachments/SendableAttachment.swift @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // -import Foundation +import AVFoundation public import SignalServiceKit /// Represents an attachment that's fully valid and ready to send.