Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/ThreemaFramework/Persistence/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 39 kB image not shown  

Quelle  EntityDestroyer.swift   Sprache: JAVA

 

import CocoaLumberjackSwift
import CoreData
import FileUtility
import Foundation

public final class EntityDestroyer: NSObject {
    
    enum EntityDestroyerError: Error {
        case entityForDescriptionNotFound
    }
    
    /// Relative to app data (group container) path.
    static let externalDataBinPath = "_EXTERNAL_DATA_BIN"

    private let objCnx: ThreemaManagedObjectContext
    private let isRemoteSecretEnabled: Bool
    
    required init(
        managedObjectContext: ThreemaManagedObjectContext,
    ) {
        self.objCnx = managedObjectContext
        self.isRemoteSecretEnabled = managedObjectContext.usesAdditionallyEncryptedModel
    }
    
    // MARK: - Public methods
    
    /// Delete media files of audio, file, image and video messages.
    ///
    /// - Parameters:
    ///   - olderThan: All message older than that date will be deleted
    ///   - conversation: ConversationEntity
    /// - Returns:
    ///   Count of deleted media files
    public func deleteMedias(olderThan: Date?, for conversation: ConversationEntity? = nil) -> Int? {
        var deletedObjects = 0
        let mediaMessageTypes = [
            AudioMessageEntity.self,
            FileMessageEntity.self,
            ImageMessageEntity.self,
            VideoMessageEntity.self,
        ]
        for mediaMessageType in mediaMessageTypes {
            if let count = deleteMediasOf(
                messageType: mediaMessageType,
                olderThan: olderThan,
                conversation: conversation
            ) {
                deletedObjects += count
            }
        }
        
        return deletedObjects
    }
    
    func deleteMediasOf(
        messageType: (some BaseMessageEntity).Type,
        olderThan: Date?,
        conversation: ConversationEntity? = nil
    ) -> Int? {
        guard messageType is AudioMessageEntity.Type ||
            messageType is FileMessageEntity.Type ||
            messageType is ImageMessageEntity.Type ||
            messageType is VideoMessageEntity.Type
        else {
            return nil
        }
        
        do {
            let mediaMetaInfo = try getMediaMetaInfo(messageType: messageType)

            if let olderThan {
                if let conversation {
                    mediaMetaInfo.fetchMessages.predicate = NSPredicate(
                        format: "conversation == %@ AND %K != nil AND date < %@",
                        conversation,
                        mediaMetaInfo.relationship,
                        olderThan as NSDate
                    )
                }
                else {
                    mediaMetaInfo.fetchMessages.predicate = NSPredicate(
                        format: "%K != nil AND date < %@",
                        mediaMetaInfo.relationship,
                        olderThan as NSDate
                    )
                }
            }
            else {
                if let conversation {
                    mediaMetaInfo.fetchMessages.predicate = NSPredicate(
                        format: "conversation == %@ AND %K != nil",
                        conversation,
                        mediaMetaInfo.relationship
                    )
                }
                else {
                    mediaMetaInfo.fetchMessages.predicate = NSPredicate(format: "%K != nil", mediaMetaInfo.relationship)
                }
            }
            
            guard let messages = try objCnx.fetch(mediaMetaInfo.fetchMessages) as? [BaseMessageEntity],
                  !messages.isEmpty else {
                return 0
            }

            try deleteMedias(of: messages, messageType: messageType)

            return messages.count
        }
        catch let error as NSError {
            DDLogError("Could not delete medias. \(error), \(error.userInfo)")
        }
        
        return 0
    }

    private func deleteMedias<T: BaseMessageEntity>(of messages: [BaseMessageEntity], messageType: T.Type) throws {
        guard messageType is AudioMessageEntity.Type ||
            messageType is FileMessageEntity.Type ||
            messageType is ImageMessageEntity.Type ||
            messageType is VideoMessageEntity.Type
        else {
            return
        }

        var deleteMediaIDs: [NSManagedObjectID] = []
        var updateMessageIDs: [NSManagedObjectID] = []

        let mediaMetaInfo = try getMediaMetaInfo(messageType: messageType)

        for message in messages {
            if let message = message as? T {
                deleteMediaIDs
                    .append(contentsOf: message.objectIDs(forRelationshipNamed: mediaMetaInfo.relationship))
                updateMessageIDs.append(message.objectID)
            }
        }

        let deleteFilenames = getExternalFilenames(ofMessages: messages, includeThumbnail: false)

        if !deleteMediaIDs.isEmpty {
            var changes = [AnyHashable: [NSManagedObjectID]]()

            // Delete media
            var confirmedDeletedIDs: [NSManagedObjectID] = []

            objCnx.propagatesDeletesAtEndOfEvent = true
            for mediaID in deleteMediaIDs {
                objCnx.performAndWait {
                    do {
                        let object = try self.objCnx.existingObject(with: mediaID)

                        if let message = object as? BaseMessageEntity {
                            nullifyConversationLastMessage(for: [message])
                        }

                        self.objCnx.delete(object)

                        try self.objCnx.save()

                        confirmedDeletedIDs.append(mediaID)
                    }
                    catch {
                        DDLogError("Could not delete file. Error: \(error); \(error.localizedDescription)")
                    }
                }
            }

            changes[NSDeletedObjectIDsKey] = confirmedDeletedIDs

            if !updateMessageIDs.isEmpty {
                var updatedIDs: [NSManagedObjectID] = []

                for updateID in updateMessageIDs {
                    if let updateMessage = try objCnx.existingObject(with: updateID) as? T {
                        objCnx.performAndWait {
                            var updated = false

                            // Update data reference to nil (if it failed to to be deleted when the object was
                            // deleted)
                            if updateMessage.value(forKey: mediaMetaInfo.relationship) != nil {
                                updateMessage.setValue(nil, forKey: mediaMetaInfo.relationship)
                                updated = true
                            }

                            // Update blobIDs to nil (to prevent downloading blob again)
                            if updateMessage.value(forKey: mediaMetaInfo.blobIDField) != nil {
                                updateMessage.setValue(nil, forKey: mediaMetaInfo.blobIDField)
                                updated = true
                            }

                            if updated {
                                do {
                                    try self.objCnx.save()

                                    updatedIDs.append(updateID)
                                }
                                catch let error as NSError {
                                    DDLogError("Cloud not update message. \(error), \(error.userInfo)")
                                }
                            }
                        }
                    }
                }

                DatabaseContext.changed(objectIDs: Set(updatedIDs))

                changes[NSUpdatedObjectsKey] = updatedIDs
            }

            deleteExternalFiles(list: deleteFilenames)
        }
    }

    /// Delete content of given message, message metadata remains.
    /// - Parameter message: Message to its content
    public func deleteMessageContent(of message: BaseMessageEntity) throws {
        guard message.typeSupportsRemoteDeletion else {
            return
        }

        // Just delete media for message types like `AudioMessageEntity`, `FileMessageEntity`, `ImageMessageEntity` and
        // `VideoMessageEntity`
        try deleteMedias(of: [message], messageType: type(of: message))
        
        // Remove all reactions
        message.userack = 0
        message.userackDate = nil
        message.groupDeliveryReceipts = nil

        if let message = message as? LocationMessageEntity {
            message.latitude = 0
            message.longitude = 0
            message.accuracy = 0
            message.poiAddress = nil
            message.poiName = nil
        }
        else if let message = message as? TextMessageEntity {
            message.text = ""
        }
        else if let message = message as? FileMessageEntity {
            deleteThumbnail(for: message)
            message.blobID = nil
            message.blobThumbnailID = nil
            message.caption = ""
            message.encryptionKey = nil
            message.fileName = ""
            message.fileSize = nil
            message.json = ""
            message.mimeType = ""
            message.origin = nil
            message.progress = nil
            message.type = nil
            message.consumed = nil
            message.thumbnail = nil
        }
        else if let message = message as? ImageMessageEntity {
            deleteThumbnail(for: message)

            message.encryptionKey = nil
            message.imageBlobID = nil
            message.imageSize = nil
            message.progress = 0
            message.imageBlobID = nil
            message.thumbnail = nil
        }
        else if let message = message as? VideoMessageEntity {
            deleteThumbnail(for: message)
            
            message.duration = 0
            message.encryptionKey = nil
            message.progress = 0
            message.videoBlobID = nil
            message.videoSize = nil
            message.duration = 0
        }
        
        if let historyEntries = message.historyEntries {
            for history in historyEntries {
                delete(entity: history)
            }
        }
        
        if let reactions = message.reactions {
            for reaction in reactions {
                delete(entity: reaction)
            }
        }
    }

    private func deleteThumbnail(for message: BaseMessageEntity) {
        guard message is FileMessageEntity || message is ImageMessageEntity || message is VideoMessageEntity else {
            return
        }

        let thumbnailObjectIDs = message.objectIDs(forRelationshipNamed: "thumbnail")
        guard !thumbnailObjectIDs.isEmpty else {
            return
        }

        for objectID in thumbnailObjectIDs {
            objCnx.performAndWait {
                do {
                    let object = try self.objCnx.existingObject(with: objectID)

                    if message is FileMessageEntity || message is ImageMessageEntity {
                        self.objCnx.delete(object)
                    }
                    else if message is VideoMessageEntity,
                            let imageData = object as? ImageDataEntity,
                            let defaultThumbnail = UIImage(systemName: "film"),
                            let data = defaultThumbnail.jpegData(compressionQuality: 0.8) {

                        imageData.data = data
                        imageData.width = Int16(defaultThumbnail.size.width)
                        imageData.height = Int16(defaultThumbnail.size.height)
                    }

                    try self.objCnx.save()
                }
                catch {
                    DDLogError("Could not delete thumbnail. Error: \(error); \(error.localizedDescription)")
                }
            }
        }
    }

    /// Delete all kind of messages.
    ///
    /// - Parameters:
    ///    - olderThan: All message older than that date will be deleted
    ///    - conversation: Conversation
    ///
    /// - Returns:
    ///    Count of deleted messages
    public func deleteMessages(olderThan: Date?, for conversation: ConversationEntity? = nil) -> Int? {
        let fetchMessages = NSFetchRequest<NSFetchRequestResult>(entityName: "Message")
        
        if let conversation {
            if let olderThan {
                fetchMessages.predicate = NSPredicate(
                    format: "conversation == %@ && date < %@",
                    conversation,
                    olderThan as NSDate
                )
            }
            else {
                fetchMessages.predicate = NSPredicate(format: "conversation == %@", conversation)
            }
        }
        else {
            if let olderThan {
                fetchMessages.predicate = NSPredicate(format: "date < %@", olderThan as NSDate)
            }
        }
        
        let deletedMessages = deleteMessages(with: fetchMessages)

        if deletedMessages > 0 {
            NotificationCenter.default.post(
                name: DatabaseContext.batchDeletedOldMessages,
                object: nil
            )
            return deletedMessages
        }
        return nil
    }

    /// Delete the Messages dated before the passed date.
    /// Currently all but open polls and starred messages are affected.
    ///
    /// - Parameters:
    ///   - olderThan: All messages older than this date are deleted
    ///   - conversationIDs: Object IDs of the conversations the messages belong to
    public func deleteMessagesForMessageRetention(olderThan: Date, for conversationsIDs: [NSManagedObjectID]) {
        guard !conversationsIDs.isEmpty else {
            return
        }
        
        self.messageRetentionDelete(olderThan: olderThan, for: conversationsIDs)
    }
    
    /// Fetch number of messages that will be deleted by message retention
    /// Currently all but open polls and starred messages are affected.
    ///
    /// - Parameters:
    ///   - olderThan: All message older than that date will be counted
    ///   - conversationsIDs: Object IDs of the conversations affected by the filter and counting
    /// - Returns: the number of messages that would be deleted. Returns 0 if there are none or if the fetch fails
    public func messagesToBeDeleted(olderThan: Date, for conversationsIDs: [NSManagedObjectID]) -> Int {
        guard !conversationsIDs.isEmpty else {
            return 0
        }
        
        do {
            let fetchRequest = try self.messageRetentionFetchRequest(olderThan: olderThan, for: conversationsIDs)
            return try self.objCnx.count(for: fetchRequest)
        }
        catch {
            DDLogError("[EntityDestroyer] Failed to count messages to be deleted: \(error)")
            return 0
        }
    }

    /// Delete all kind of messages within conversation.
    ///
    /// - Parameters:
    ///    - conversation: Delete all message of this ConversationEntity
    ///
    /// - Returns:
    ///    Count of deleted messages
    public func deleteMessages(of conversation: ConversationEntity) -> Int {
        let fetchMessages = NSFetchRequest<NSFetchRequestResult>(entityName: "Message")
        fetchMessages.predicate = NSPredicate(format: "conversation = %@", conversation)
        
        let deletedMessages = deleteMessages(with: fetchMessages)
        if deletedMessages > 0 {
            NotificationCenter.default.post(
                name: DatabaseContext.batchDeletedAllConversationMessages,
                object: nil,
                userInfo: [DatabaseContext.conversationObjectIDKey: conversation.objectID]
            )
        }
        return deletedMessages
    }

    @objc public func delete(ballot: BallotEntity) {
        delete(entity: ballot)
    }
    
    public func delete(reaction: MessageReactionEntity) {
        delete(entity: reaction)
    }
    
    func delete(ballotChoice: BallotChoiceEntity) {
        delete(entity: ballotChoice)
    }

    @objc public func delete(baseMessage: BaseMessageEntity) {
        delete(entity: baseMessage)
    }

    public func delete(callEntity: CallEntity) {
        delete(entity: callEntity)
    }

    public func delete(conversation: ConversationEntity) {
        delete(entity: conversation)
    }

    #if DEBUG
        @available(*, deprecated, message: "Use this function only for testing!")
        /// - Parameter contactEntity: Core Data object
        public func delete(contactEntity: ContactEntity) {
            delete(entity: contactEntity)
        }
    #endif

    public func delete(distributionListEntity: DistributionListEntity) {
        delete(entity: distributionListEntity)
    }

    public func delete(groupCallEntity: GroupCallEntity) {
        delete(entity: groupCallEntity)
    }

    func delete(groupEntity: GroupEntity) {
        delete(entity: groupEntity)
    }

    public func delete(imageDataEntity: ImageDataEntity) {
        delete(entity: imageDataEntity)
    }

    public func delete(lastGroupSyncRequestEntity: LastGroupSyncRequestEntity) {
        delete(entity: lastGroupSyncRequestEntity)
    }

    public func delete(messageHistoryEntryEntity: MessageHistoryEntryEntity) {
        delete(entity: messageHistoryEntryEntity)
    }

    public func delete(webClientSessionEntity: WebClientSessionEntity) {
        delete(entity: webClientSessionEntity)
    }
    
    @objc public func delete(workAvailabilityStatus: WorkAvailabilityStatusEntity) {
        delete(entity: workAvailabilityStatus)
    }

    /// Delete particular DB object.
    ///
    /// - Parameters:
    ///    - object: object to delete
    private func delete(entity object: NSManagedObject) {
        var shouldUpdateConversationContent = false
        if let conversation = object as? ConversationEntity {
            let count = deleteMessages(of: conversation)
            DDLogDebug("\(count) messages deleted from conversation")
        }
        else if let contact = object as? ContactEntity {
            // Remove all conversations and messages for this contact
            if let conversations = contact.conversations {
                for conversation in conversations {
                    delete(entity: conversation)
                }
            }
            
            // check all group conversations to delete messages from this contact
            let count = deleteMessages(for: contact)
            DDLogDebug("\(count) group messages deleted from contact")

            if count > 0 {
                shouldUpdateConversationContent = true
            }
        }
        else if let distributionList = object as? DistributionListEntity {
            delete(entity: distributionList.conversation)
        }
                
        let deleteFilenames = getExternalFilenames(ofMessages: [object], includeThumbnail: true)

        deleteExternalFiles(list: deleteFilenames)

        if let message = object as? BaseMessageEntity {
            nullifyConversationLastMessage(for: [message])
        }
        
        objCnx.delete(object)
        
        if shouldUpdateConversationContent {
            // Update open chats content
            DispatchQueue.main.async {
                NotificationCenter.default.post(
                    name: DatabaseContext.batchDeletedOldMessages,
                    object: nil,
                    userInfo: nil
                )
            }
        }
    }

    /// Delete 1:1 conversation of a contact.
    ///
    /// - Parameters:
    ///   - contactEntity: Delete 1:1 conversation of this contact
    ///   - conversationWillBeDeleted: Called before the conversation will be delete
    public func deleteOneToOneConversation(
        for contactEntity: ContactEntity,
        conversationWillBeDeleted: (ConversationEntity) -> Void
    ) {
        if let conversations = contactEntity.conversations {
            for conversation in conversations.filter({ !$0.isGroup }) {
                conversationWillBeDeleted(conversation)
                delete(conversation: conversation)
            }
        }
    }

    /// Delete `ContactEntity` of own identity.
    ///
    /// - Parameter identity: The identity of the local user
    /// - Returns: True own contact was found and deleted
    public func deleteOwnContact(myIdentity: String?) -> Bool {
        guard let myIdentity else {
            DDLogError("[EntityDestroyer] Own contact cannot be deleted because no Threema-ID is in the keychain")
            return false
        }
        
        let fetchContacts = NSFetchRequest<NSFetchRequestResult>(entityName: "Contact")
        fetchContacts.predicate = NSPredicate(format: "identity = %@", myIdentity)

        guard let entities = try? objCnx.fetch(fetchContacts) as? [ContactEntity], !entities.isEmpty else {
            DDLogNotice("[EntityDestroyer] No own contact found to delete")
            return false
        }

        for entity in entities {
            delete(entity: entity)
        }

        return true
    }

    /// Get orphaned external files.
    ///
    /// - Returns: List of orphaned files, count of files in DB
    public func orphanedExternalFiles(appGroupID: String) -> (orphanedFiles: [String], totalFilesCount: Int) {
        
        // Load all external filenames
        if let files = FileUtility.shared
            .dir(
                at: FileUtility.shared.appDataDirectory(appGroupID: appGroupID)?
                    .appendingPathComponent("\(DatabaseManager.databaseExternalStoragePath)/")
            ),
            !files.isEmpty {
            
            // Load all filenames from DB
            var filesInDB = [String]()
            do {
                let fetchRequests: [NSFetchRequest<NSManagedObject>] = [
                    "AudioData",
                    "FileData",
                    "ImageData",
                    "VideoData",
                ]
                .map { NSFetchRequest<NSManagedObject>(entityName: $0) }
                
                for fetchRequest in fetchRequests {
                    try autoreleasepool {
                        let totalItems = try objCnx.count(for: fetchRequest)
                        let increment = 20
                        let sequence = stride(from: 0, to: totalItems, by: increment)
                        for i in sequence {
                            try autoreleasepool {
                                fetchRequest.fetchOffset = i
                                fetchRequest.fetchLimit = increment
                                
                                let items = try objCnx.fetch(fetchRequest)
                                for item in items {
                                    objCnx.refresh(item, mergeChanges: true)
                                    
                                    if let externalStorageInfo = item as? ExternalStorageInfo {
                                        if let filename = externalStorageInfo.getFilename() {
                                            filesInDB.append(filename)
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            catch let error as NSError {
                DDLogError("Could not load filenames from DB. \(error), \(error.userInfo)")
            }
            
            // Filter orphaned external files
            let orphanedFiles = files.map(\.lastPathComponent)
                .filter { file -> Bool in
                    !filesInDB.contains(file)
                }
            #if DEBUG
                for file in orphanedFiles {
                    DDLogInfo("Orphaned file: \(file)")
                }
            #endif
            
            return (orphanedFiles, filesInDB.count)
        }
        
        return ([], 0)
    }
    
    public func deleteMissedCallsCacheOlderThanTwoWeeks() {
        guard let twoWeeksAgo = Calendar.current.date(byAdding: .day, value: -14, to: Date()) as? NSDate else {
            fatalError()
        }
        
        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Call")
        fetchRequest.predicate = NSPredicate(format: "date <= %@", twoWeeksAgo)
        
        do {
            let batch = NSBatchDeleteRequest(fetchRequest: fetchRequest)
            batch.resultType = NSBatchDeleteRequestResultType.resultTypeObjectIDs
            let deleteResult = try objCnx.execute(batch) as? NSBatchDeleteResult
            
            if let deletedIDs = deleteResult?.result as? [NSManagedObjectID], !deletedIDs.isEmpty {
                DatabaseContext.mergeChangesIntoMainContext([NSDeletedObjectsKey: deletedIDs])
                DatabaseContext.mergeChangesIntoDirectContexts([NSDeletedObjectsKey: deletedIDs])
            }
            else {
                // Fallback for when the batch delete request misses the objects stored in the DB
                // This should only happen in unit tests but if it were to ever change and we'd miss it
                // well still be safe in production.
                guard let fetchedCalls = try? fetchRequest.execute() as? [NSManagedObject] else {
                    DDLogError("Could not delete calls cache. Unknown error.")
                    return
                }
                
                for fetchedCall in fetchedCalls {
                    delete(entity: fetchedCall)
                }
            }
        }
        catch let error as NSError {
            DDLogError("Could not delete calls cache. \(error), \(error.userInfo)")
        }
    }
    
    // MARK: - Private helper methods
    
    private func messageRetentionFetchRequest(
        olderThan: Date,
        for conversationIDs: [NSManagedObjectID]
    ) throws -> NSFetchRequest<NSFetchRequestResult> {
        guard let ballotMessageEntity = NSEntityDescription.entity(forEntityName: "BallotMessage", in: objCnx) else {
            throw EntityDestroyerError.entityForDescriptionNotFound
        }
        
        let conversationAndDatePredicate = NSPredicate(
            format: "conversation IN %@ AND date < %@",
            conversationIDs,
            olderThan as NSDate
        )
        // A ballot message is kept as long as its ballot is not closed
        let isOpenBallotMessagePredicate = NSPredicate.and(
            NSPredicate(format: "self.entity == %@", ballotMessageEntity),
            .or(
                NSPredicate(format: "ballotState == nil"),
                NSPredicate(format: "ballotState != %d", BallotEntity.BallotState.closed.rawValue)
            )
        )
        let isStarredPredicate = NSPredicate.and(
            NSPredicate(format: "messageMarkers != nil"),
            NSPredicate(format: "messageMarkers.star == YES")
        )

        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Message")
        fetchRequest.predicate = .and(
            conversationAndDatePredicate,
            .not(.or(isOpenBallotMessagePredicate, isStarredPredicate))
        )
        
        return fetchRequest
    }

    private func messageRetentionDelete(olderThan: Date, for conversationIDs: [NSManagedObjectID]) {
        guard !conversationIDs.isEmpty else {
            return
        }

        do {
            let fetchRequest = try messageRetentionFetchRequest(olderThan: olderThan, for: conversationIDs)
            fetchRequest.resultType = .managedObjectIDResultType

            let objectIDs = try objCnx.performAndWait {
                try objCnx.fetch(fetchRequest) as? [NSManagedObjectID] ?? []
            }

            guard !objectIDs.isEmpty else {
                DDLogNotice("[EntityDestroyer]: No messages to delete by automatic retention")
                return
            }
            
            // Note: With a batch delete sometimes the external file of a file message will not be deleted immediately.
            // If the external file is remaining, than the only way to delete it, is in Settings - Advanced - Delete
            // Orphaned Files
            let batch = NSBatchDeleteRequest(objectIDs: objectIDs)
            batch.resultType = .resultTypeObjectIDs

            // Update last message of affected conversations and external files of the deleted messages
            nullifyConversationLastMessage(forObjectIDs: objectIDs)
            let deleteFilenames = getExternalFilenames(ofObjectIDs: objectIDs, includeThumbnail: true)
            deleteExternalFiles(list: deleteFilenames)
                                
            if let deleteResult = try objCnx.execute(batch) as? NSBatchDeleteResult,
               let deletedIDs = deleteResult.result as? [NSManagedObjectID], !deletedIDs.isEmpty {

                refreshDatabaseMainAndDirectContexts(for: deletedIDs)
                Task.detached {
                    DatabaseContext.changed(objectIDs: Set(deletedIDs))
                }
                
                DDLogNotice(
                    "[EntityDestroyer]: Deleted \(deletedIDs.count) of \(objectIDs.count) messages by automatic retention"
                )
                NotificationCenter.default.post(
                    name: DatabaseContext.batchDeletedOldMessages,
                    object: nil
                )
            }
        }
        catch {
            DDLogError("[EntityDestroyer]: Could not delete messages by automatic retention: \(error)")
        }
    }
    
    private func getMediaMetaInfo(
        messageType: (some Any)
            .Type
    ) throws -> (fetchMessages: NSFetchRequest<NSManagedObject>, relationship: String, blobIDField: String) {
        
        var fetchMessages: NSFetchRequest<NSManagedObject>
        var relationship: String
        var blobIDField: String

        if messageType is AudioMessageEntity.Type {
            fetchMessages = NSFetchRequest<NSManagedObject>(entityName: "AudioMessage")
            relationship = AudioMessageEntity.Field.name(for: .audio, encrypted: isRemoteSecretEnabled)
            blobIDField = AudioMessageEntity.Field.name(for: .audioBlobID, encrypted: isRemoteSecretEnabled)
        }
        else if messageType is FileMessageEntity.Type {
            fetchMessages = NSFetchRequest<NSManagedObject>(entityName: "FileMessage")
            relationship = FileMessageEntity.Field.name(for: .data, encrypted: isRemoteSecretEnabled)
            blobIDField = FileMessageEntity.Field.name(for: .blobID, encrypted: isRemoteSecretEnabled)
        }
        else if messageType is ImageMessageEntity.Type {
            fetchMessages = NSFetchRequest<NSManagedObject>(entityName: "ImageMessage")
            relationship = ImageMessageEntity.Field.name(for: .image, encrypted: isRemoteSecretEnabled)
            blobIDField = ImageMessageEntity.Field.name(for: .imageBlobID, encrypted: isRemoteSecretEnabled)
        }
        else if messageType is VideoMessageEntity.Type {
            fetchMessages = NSFetchRequest<NSManagedObject>(entityName: "VideoMessage")
            relationship = VideoMessageEntity.Field.name(for: .video, encrypted: isRemoteSecretEnabled)
            blobIDField = VideoMessageEntity.Field.name(for: .videoBlobID, encrypted: isRemoteSecretEnabled)
        }
        else {
            fatalError("message type not defined")
        }
        
        return (fetchMessages, relationship, blobIDField)
    }
    
    private func getExternalFilenames(ofObjectIDs: [NSManagedObjectID], includeThumbnail: Bool) -> [String] {
        var externalFilenames = [String]()

        do {
            for entityName in ["AudioMessage", "FileMessage", "ImageMessage", "VideoMessage"] {
                let fetch = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
                fetch.predicate = NSPredicate(format: "self IN %@", ofObjectIDs)

                let messages = try objCnx.fetch(fetch)

                externalFilenames.append(
                    contentsOf: getExternalFilenames(ofMessages: messages, includeThumbnail: includeThumbnail)
                )
            }
        }
        catch {
            DDLogError("Fetching messages to evaluate external files failed: \(error)")
        }

        return externalFilenames
    }

    /// List names of external files.
    ///
    /// - Parameters:
    ///    - ofMessages: Check messages to external data files
    ///    - includeThumbnail: Check messages to external thumbnail files
    ///
    /// - Returns: Names of external files
    private func getExternalFilenames(ofMessages: [Any], includeThumbnail: Bool) -> [String] {
        var externalFilenames: [String] = []
        for message in ofMessages {
            guard message is AudioMessageEntity || message is FileMessageEntity || message is ImageMessageEntity ||
                message is VideoMessageEntity else {
                continue
            }
                
            // Refreshing media objects, otherwise external filenames can not be evaluated for new messages
            let mediaMetaInfo: (
                fetchMessages: NSFetchRequest<NSManagedObject>,
                relationship: String,
                blobIDField: String
                // swiftformat:disable:next wrapMultilineConditionalAssignment
            )? = switch message {
            case is AudioMessageEntity:
                try? getMediaMetaInfo(messageType: AudioMessageEntity.self)
            case is FileMessageEntity:
                try? getMediaMetaInfo(messageType: FileMessageEntity.self)
            case is ImageMessageEntity:
                try? getMediaMetaInfo(messageType: ImageMessageEntity.self)
            case is VideoMessageEntity:
                try? getMediaMetaInfo(messageType: VideoMessageEntity.self)
            default:
                nil
            }

            if let relationship = mediaMetaInfo?.relationship,
               let message = message as? NSManagedObject {
                
                var mediaIDs = [NSManagedObjectID]()
                mediaIDs.append(contentsOf: message.objectIDs(forRelationshipNamed: relationship))

                for mediaID in mediaIDs {
                    if let mediaObj = try? objCnx.existingObject(with: mediaID) {
                        objCnx.refresh(mediaObj, mergeChanges: true)
                    }
                }
            }

            // Get external file name
            if let audioMessage = message as? AudioMessageEntity {
                if let externalFileName = audioMessage.audio?.getFilename() {
                    externalFilenames.append(externalFileName)
                }
            }
            else if let fileMessage = message as? FileMessageEntity {
                if let externalFileName = fileMessage.data?.getFilename() {
                    externalFilenames.append(externalFileName)
                }
                if includeThumbnail, let thumbnailFileName = fileMessage.thumbnail?.getFilename() {
                    externalFilenames.append(thumbnailFileName)
                }
            }
            else if let imageMessage = message as? ImageMessageEntity {
                if let externalFileName = imageMessage.image?.getFilename() {
                    externalFilenames.append(externalFileName)
                }
                if includeThumbnail, let thumbnailFileName = imageMessage.thumbnail?.getFilename() {
                    externalFilenames.append(thumbnailFileName)
                }
            }
            else if let videoMessage = message as? VideoMessageEntity {
                if let externalFileName = videoMessage.video?.getFilename() {
                    externalFilenames.append(externalFileName)
                }
                if includeThumbnail, let thumbnailFileName = videoMessage.thumbnail?.getFilename() {
                    externalFilenames.append(thumbnailFileName)
                }
            }
        }
        return externalFilenames
    }

    /// Delete external files.
    ///
    /// - Parameters:
    ///    - list: List of filenames to delete
    private func deleteExternalFiles(list: [String]) {
        // It seams, since iOS version 15 is this not necessary anymore. Core Data deleting now the external files.
        // If we do that yet Core Data crashes with: "External data reference can't find underlying file."
        // swiftformat:disable:next all
        return;

//        if !list.isEmpty {
//            for filename in list {
//                let fileURL = FileUtility.shared.appDataDirectory(appGroupID: AppGroup.groupID())?
//                    .appendingPathComponent("\(DatabaseManager.databaseExternalStoragePath)/\(filename)")
//                FileUtility.shared.deleteIfExists(at: fileURL)
//            }
//        }
    }
    
    /// Delete all kind of messages of this contact.
    ///
    /// - Parameters:
    ///    - contact: Delete all messages of this contact
    ///
    /// - Returns:
    ///    Count of deleted messages
    private func deleteMessages(for contact: ContactEntity) -> Int {
        let fetchMessages = NSFetchRequest<NSFetchRequestResult>(entityName: "Message")
        fetchMessages.predicate = NSPredicate(format: "sender = %@", contact)
        return deleteMessages(with: fetchMessages)
    }
    
    /// Delete all kind of messages of the fetch request.
    ///
    /// - Parameters:
    ///    - fetchRequest: NSFetchRequest<NSFetchRequestResult>
    ///
    /// - Returns:
    ///    Count of deleted messages
    private func deleteMessages(with fetchRequest: NSFetchRequest<NSFetchRequestResult>) -> Int {
        do {
            fetchRequest.resultType = NSFetchRequestResultType.managedObjectIDResultType
            guard let objectIDs = try objCnx.fetch(fetchRequest) as? [NSManagedObjectID], !objectIDs.isEmpty else {
                return 0
            }

            let deleteFilenames = getExternalFilenames(ofObjectIDs: objectIDs, includeThumbnail: true)

            nullifyConversationLastMessage(forObjectIDs: objectIDs)

            // With a batch delete sometimes the external file of a file message will not be deleted immediately. If the
            // external file is remaining, than the only way to delete it, is in Settings - Advanced - Delete Orphaned
            // Files
            let batch = NSBatchDeleteRequest(fetchRequest: fetchRequest)
            batch.resultType = NSBatchDeleteRequestResultType.resultTypeObjectIDs
            
            if let deleteResult = try objCnx.execute(batch) as? NSBatchDeleteResult,
               let deletedIDs = deleteResult.result as? [NSManagedObjectID], !deletedIDs.isEmpty {
                refreshDatabaseMainAndDirectContexts(for: deletedIDs)

                deleteExternalFiles(list: deleteFilenames)

                return deletedIDs.count
            }
            
            return 0
        }
        catch let error as NSError {
            DDLogError("[Entity Destroyer] Could not delete messages. \(error), \(error.userInfo)")
        }

        return 0
    }

    private func nullifyConversationLastMessage(for messages: [BaseMessageEntity]) {
        guard !messages.isEmpty else {
            return
        }

        nullifyConversationLastMessage(forObjectIDs: messages.map(\.objectID))
    }

    private func nullifyConversationLastMessage(forObjectIDs: [NSManagedObjectID]) {
        guard !forObjectIDs.isEmpty else {
            return
        }

        do {
            try objCnx.performAndWait {
                let fetchConversations = NSFetchRequest<NSFetchRequestResult>(entityName: "Conversation")
                fetchConversations.predicate = NSPredicate(format: "lastMessage IN %@", forObjectIDs)

                if let conversations = try fetchConversations.execute() as? [ConversationEntity],
                   !conversations.isEmpty {
                    DDLogNotice("[Entity Destroyer]: Reset las message for \(conversations.count) conversation")
                    for conversation in conversations {
                        conversation.lastMessage = nil
                    }

                    try self.objCnx.save()
                }
            }
        }
        catch {
            DDLogError("[Entity Destroyer]: Failed to nullify `Conversation.lastMessage`: \(error)")
        }
    }

    /// Merge deleted objects in private (background) context to main and direct database contexts.
    /// - Parameter deletedIDs: Array with object ID of deleted objects
    private func refreshDatabaseMainAndDirectContexts(for deletedIDs: [NSManagedObjectID]) {
        guard !deletedIDs.isEmpty else {
            return
        }

        // Merge the deletions into the app's managed object context.
        let deletedObjects = [NSDeletedObjectsKey: deletedIDs]
        DatabaseContext.mergeChangesIntoMainContext(deletedObjects)
        DatabaseContext.mergeChangesIntoDirectContexts(deletedObjects)
    }
}

Messung
CHZG
919410094

¤ Dauer der Verarbeitung: 0.8 Sekunden  (vorverarbeitet am  2026-11-04) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.