diff --git a/Packages/ComposeUI/Sources/ComposeUI/CoreData/DraftAttachment.swift b/Packages/ComposeUI/Sources/ComposeUI/CoreData/DraftAttachment.swift index 7be3c945..0658e3c4 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/CoreData/DraftAttachment.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/CoreData/DraftAttachment.swift @@ -18,6 +18,10 @@ private let encoder = PropertyListEncoder() @objc public final class DraftAttachment: NSManagedObject, Identifiable { + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "DraftAttachment") + } + @NSManaged internal var assetID: String? @NSManaged public var attachmentDescription: String @NSManaged internal private(set) var drawingData: Data? @@ -154,9 +158,13 @@ extension DraftAttachment: NSItemProviderReading { return attachment } - static func writeDataToFile(_ data: Data, id: UUID, type: UTType) throws -> URL { + static var attachmentsDirectory: URL { let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")! - let directoryURL = containerURL.appendingPathComponent("Documents").appendingPathComponent("attachments") + return containerURL.appendingPathComponent("Documents").appendingPathComponent("attachments") + } + + static func writeDataToFile(_ data: Data, id: UUID, type: UTType) throws -> URL { + let directoryURL = attachmentsDirectory try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true) let attachmentURL = directoryURL.appendingPathComponent(id.uuidString, conformingTo: type) try data.write(to: attachmentURL) diff --git a/Packages/ComposeUI/Sources/ComposeUI/CoreData/DraftsPersistentContainer.swift b/Packages/ComposeUI/Sources/ComposeUI/CoreData/DraftsPersistentContainer.swift index 0e549fd6..6a86798d 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/CoreData/DraftsPersistentContainer.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/CoreData/DraftsPersistentContainer.swift @@ -157,6 +157,29 @@ public class DraftsPersistentContainer: NSPersistentContainer { draft.attachments.add(draftAttachment) } + public func removeOrphanedAttachments(completion: @escaping () -> Void) { + guard let files = try? FileManager.default.contentsOfDirectory(at: DraftAttachment.attachmentsDirectory, includingPropertiesForKeys: nil), + !files.isEmpty else { + return + } + performBackgroundTask { context in + let allAttachmentsReq = DraftAttachment.fetchRequest() + allAttachmentsReq.predicate = NSPredicate(format: "fileURL != nil") + guard let allAttachments = try? context.fetch(allAttachmentsReq) else { + return + } + let orphaned = Set(files).subtracting(allAttachments.lazy.compactMap(\.fileURL)) + for url in orphaned { + do { + try FileManager.default.removeItem(at: url) + } catch { + logger.error("Failed to remove orphaned attachment: \(String(describing: error), privacy: .public)") + } + } + completion() + } + } + @objc private func remoteChanges(_ notification: Foundation.Notification) { guard let newHistoryToken = notification.userInfo?[NSPersistentHistoryTokenKey] as? NSPersistentHistoryToken else { return diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 503933ad..29f1803c 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -303,6 +303,7 @@ D6D79F2F2A0D6A7F00AB2315 /* StatusEditPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */; }; D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */; }; D6D79F572A1160B800AB2315 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */; }; + D6D79F592A13293200AB2315 /* BackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F582A13293200AB2315 /* BackgroundManager.swift */; }; D6D94955298963A900C59229 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D94954298963A900C59229 /* Colors.swift */; }; D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */; }; D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.swift */; }; @@ -704,6 +705,7 @@ D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditPollView.swift; sourceTree = ""; }; D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableButton.swift; sourceTree = ""; }; D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = ""; }; + D6D79F582A13293200AB2315 /* BackgroundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundManager.swift; sourceTree = ""; }; D6D94954298963A900C59229 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusCollectionViewCell.swift; sourceTree = ""; }; D6D9498E298EB79400C59229 /* CopyableLable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLable.swift; sourceTree = ""; }; @@ -1476,6 +1478,7 @@ D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */, D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */, D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */, + D6D79F582A13293200AB2315 /* BackgroundManager.swift */, D61F75B6293C119700C0B37F /* Filterer.swift */, D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */, D61F75BA293C183100C0B37F /* HTMLConverter.swift */, @@ -2091,6 +2094,7 @@ D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */, D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */, D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */, + D6D79F592A13293200AB2315 /* BackgroundManager.swift in Sources */, D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */, D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */, D6F6A550291F058600F496A8 /* CreateListService.swift in Sources */, diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 1751e9e7..6b3c2ec5 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -76,6 +76,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } + BackgroundManager.shared.registerHandlers() + return true } diff --git a/Tusker/BackgroundManager.swift b/Tusker/BackgroundManager.swift new file mode 100644 index 00000000..931de720 --- /dev/null +++ b/Tusker/BackgroundManager.swift @@ -0,0 +1,45 @@ +// +// BackgroundManager.swift +// Tusker +// +// Created by Shadowfacts on 5/15/23. +// Copyright © 2023 Shadowfacts. All rights reserved. +// + +import Foundation +import BackgroundTasks +import OSLog +import ComposeUI + +private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "BackgroundManager") + +struct BackgroundManager { + static let shared = BackgroundManager() + + private init() {} + + static let cleanupAttachmentsIdentifier = "\(Bundle.main.bundleIdentifier!).cleanup-attachments" + + func scheduleTasks() { + BGTaskScheduler.shared.getPendingTaskRequests { requests in + if !requests.contains(where: { $0.identifier == Self.cleanupAttachmentsIdentifier }) { + let request = BGProcessingTaskRequest(identifier: Self.cleanupAttachmentsIdentifier) + do { + try BGTaskScheduler.shared.submit(request) + } catch { + logger.error("Failed to schedule cleanup attachments: \(String(describing: error), privacy: .public)") + } + } + } + } + + func registerHandlers() { + BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.cleanupAttachmentsIdentifier, using: nil, launchHandler: handleCleanupAttachments(_:)) + } + + private func handleCleanupAttachments(_ task: BGTask) { + DraftsPersistentContainer.shared.removeOrphanedAttachments() { + task.setTaskCompleted(success: true) + } + } +} diff --git a/Tusker/Info.plist b/Tusker/Info.plist index c49a8efe..2f03cd02 100644 --- a/Tusker/Info.plist +++ b/Tusker/Info.plist @@ -2,8 +2,10 @@ - ITSAppUsesNonExemptEncryption - + BGTaskSchedulerPermittedIdentifiers + + $(PRODUCT_BUNDLE_IDENTIFIER).cleanup-attachments + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -33,6 +35,8 @@ CFBundleVersion $(CURRENT_PROJECT_VERSION) + ITSAppUsesNonExemptEncryption + LSApplicationCategoryType public.app-category.social-networking LSApplicationQueriesSchemes @@ -141,6 +145,7 @@ UIBackgroundModes + processing remote-notification UILaunchStoryboardName diff --git a/Tusker/Scenes/MainSceneDelegate.swift b/Tusker/Scenes/MainSceneDelegate.swift index 4e657dad..984bc8b7 100644 --- a/Tusker/Scenes/MainSceneDelegate.swift +++ b/Tusker/Scenes/MainSceneDelegate.swift @@ -159,6 +159,8 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate try? context.save() } + + BackgroundManager.shared.scheduleTasks() } func showAppOrOnboardingUI(session: UISceneSession? = nil) {