Skip to content

Commit

Permalink
Add methods for deleting local downloads and clear downloaded files o…
Browse files Browse the repository at this point in the history
…n logout
  • Loading branch information
laevandus committed Aug 23, 2024
1 parent ba6b8f2 commit e6564af
Show file tree
Hide file tree
Showing 12 changed files with 157 additions and 7 deletions.
14 changes: 10 additions & 4 deletions DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -519,12 +519,18 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
}
}
}),
.init(title: "Delete Downloaded Attachments", handler: { _ in
guard FileManager.default.fileExists(atPath: URL.streamAttachmentDownloadsDirectory.path) else { return }
.init(title: "Delete Downloaded Attachments", handler: { [unowned self] _ in
do {
try FileManager.default.removeItem(at: .streamAttachmentDownloadsDirectory)
let connectedUser = try self.rootViewController.controller.client.makeConnectedUser()
Task {
do {
try await connectedUser.deleteAllAttachmentLocalDownloads()
} catch {
self.rootViewController.presentAlert(title: error.localizedDescription)
}
}
} catch {
log.debug("Failed to delete downloaded attachments with error: \(error.localizedDescription)")
self.rootViewController.presentAlert(title: error.localizedDescription)
}
})
])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,18 @@ public extension CurrentChatUserController {
}
}
}

/// Deletes all the local downloads of file attachments.
///
/// - Parameter completion: Called when files have been deleted or when an error occured.
func deleteAllAttachmentLocalDownloads(completion: ((Error?) -> Void)? = nil) {
currentUserUpdater.deleteAllAttachmentLocalDownloads { error in
guard let completion else { return }
self.callback {
completion(error)
}
}
}

/// Fetches all the unread information from the current user.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,24 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
/// - id: The id of the file attachment.
/// - completion: A completion block with a file attachment containing the downloading state.
public func downloadAttachment(with id: AttachmentId, completion: @escaping (Result<ChatMessageFileAttachment, Error>) -> Void) {
messageUpdater.downloadAttachment(with: id, completion: completion)
messageUpdater.downloadAttachment(with: id) { result in
self.callback {
completion(result)
}
}
}

/// Deletes the locally downloaded file.
///
/// - Parameters:
/// - attachmentId: The id of the attachment.
/// - completion: A completion block with an error if the deletion failed.
public func deleteLocalAttachmentDownload(for attachmentId: AttachmentId, completion: ((Error?) -> Void)? = nil) {
messageUpdater.deleteLocalAttachmentDownload(for: attachmentId) { error in
self.callback {
completion?(error)
}
}
}

/// Updates local state of attachment with provided `id` to be enqueued by attachment uploader.
Expand Down
18 changes: 18 additions & 0 deletions Sources/StreamChat/Database/DTOs/AttachmentDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ class AttachmentDTO: NSManagedObject {
return new
}

static func downloadedFetchRequest() -> NSFetchRequest<AttachmentDTO> {
let request = NSFetchRequest<AttachmentDTO>(entityName: AttachmentDTO.entityName)
request.sortDescriptors = [NSSortDescriptor(keyPath: \AttachmentDTO.id, ascending: true)]
request.predicate = NSPredicate(format: "localStateRaw == %@", LocalAttachmentState.downloaded.rawValue)
return request
}

static func pendingUploadFetchRequest() -> NSFetchRequest<AttachmentDTO> {
let request = NSFetchRequest<AttachmentDTO>(entityName: AttachmentDTO.entityName)
request.sortDescriptors = [NSSortDescriptor(keyPath: \AttachmentDTO.id, ascending: true)]
Expand All @@ -93,6 +100,13 @@ class AttachmentDTO: NSManagedObject {
request.predicate = NSPredicate(format: "localStateRaw == %@", LocalAttachmentState.uploading(progress: 0).rawValue)
return load(by: request, context: context)
}

static func loadAllDownloadedAttachments(context: NSManagedObjectContext) -> [AttachmentDTO] {
let request = NSFetchRequest<AttachmentDTO>(entityName: AttachmentDTO.entityName)
request.sortDescriptors = [NSSortDescriptor(keyPath: \AttachmentDTO.id, ascending: true)]
request.predicate = NSPredicate(format: "localStateRaw == %@", LocalAttachmentState.downloaded.rawValue)
return load(by: request, context: context)
}
}

extension AttachmentDTO: EphemeralValuesContainer {
Expand Down Expand Up @@ -161,6 +175,10 @@ extension NSManagedObjectContext: AttachmentDatabaseSession {
func delete(attachment: AttachmentDTO) {
delete(attachment)
}

func allLocallyDownloadedAttachments() -> [AttachmentDTO] {
AttachmentDTO.loadAllDownloadedAttachments(context: self)
}
}

private extension AttachmentDTO {
Expand Down
8 changes: 8 additions & 0 deletions Sources/StreamChat/Database/DatabaseContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,14 @@ class DatabaseContainer: NSPersistentContainer {
context.reset()
}
}

if FileManager.default.fileExists(atPath: URL.streamAttachmentDownloadsDirectory.path) {
do {
try FileManager.default.removeItem(at: .streamAttachmentDownloadsDirectory)
} catch {
log.debug("Failed to remove local downloads", subsystems: .database)
}
}
}
self?.canWriteData = true
completion?(lastEncounteredError)
Expand Down
3 changes: 3 additions & 0 deletions Sources/StreamChat/Database/DatabaseSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,9 @@ protocol AttachmentDatabaseSession {
/// Deletes the provided dto from a database
/// - Parameter attachment: The DTO to be deleted
func delete(attachment: AttachmentDTO)

/// All the attachments with the local status being downloaded.
func allLocallyDownloadedAttachments() -> [AttachmentDTO]
}

protocol QueuedRequestDatabaseSession {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ extension FileAttachmentPayload: Decodable {

extension URL {
/// The directory URL for attachment downloads.
public static var streamAttachmentDownloadsDirectory: URL {
static var streamAttachmentDownloadsDirectory: URL {
(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first ?? FileManager.default.temporaryDirectory)
.appendingPathComponent("AttachmentDownloads", isDirectory: true)
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/StreamChat/StateLayer/Chat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -363,11 +363,22 @@ public class Chat {
/// - Note: The local storage URL can change between app launches.
///
/// - Parameter attachmentId: The id of the attachment.
///
/// - Throws: An error while downloading an attachment.
/// - Returns: An instance of the downloaded file attachment.
@discardableResult public func downloadAttachment(with attachmentId: AttachmentId) async throws -> ChatMessageFileAttachment {
try await messageUpdater.downloadAttachment(with: attachmentId)
}

/// Deletes the locally downloaded file.
///
/// - Parameter attachmentId: The id of the attachment.
///
/// - Throws: An error while deleting a downloaded file.
public func deleteLocalAttachmentDownload(for attachmentId: AttachmentId) async throws {
try await messageUpdater.deleteLocalAttachmentDownload(for: attachmentId)
}

/// Resends a failed attachment.
///
/// - Parameter attachmentId: The id of the attachment.
Expand Down
11 changes: 11 additions & 0 deletions Sources/StreamChat/StateLayer/ConnectedUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,17 @@ public final class ConnectedUser {
try await userUpdater.unflag(userId)
}

// MARK: Managing Local Attachment Downloads

/// Deletes all the local downloads of file attachments.
///
/// - Parameter completion: Called when files have been deleted or when an error occured.
///
/// - Throws: An error while deleting local downloads.
public func deleteAllAttachmentLocalDownloads() async throws {
try await currentUserUpdater.deleteAllAttachmentLocalDownloads()
}

// MARK: - Private

private func currentUserId() throws -> UserId {
Expand Down
35 changes: 35 additions & 0 deletions Sources/StreamChat/Workers/CurrentUserUpdater.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,33 @@ class CurrentUserUpdater: Worker {
}
}
}

func deleteAllAttachmentLocalDownloads(completion: @escaping (Error?) -> Void) {
database.write({ session in
// Try to delete all the local files even when one of them happens to fail.
var latestError: Error?
let attachments = session.allLocallyDownloadedAttachments()
for attachment in attachments {
if let localRelativePath = attachment.localRelativePath {
let localURL = ChatMessageFileAttachment.localStorageURL(forRelativePath: localRelativePath)
if FileManager.default.fileExists(atPath: localURL.path) {
do {
try FileManager.default.removeItem(at: localURL)
} catch {
latestError = error
}
}
}
attachment.localState = nil
attachment.localRelativePath = nil
attachment.localState = nil
attachment.localURL = nil
}
log.info("Deleted local downloads for number of attachments: \(attachments.count)", subsystems: .database)
guard let latestError else { return }
throw latestError
}, completion: completion)
}

/// Marks all channels for a user as read.
/// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails.
Expand Down Expand Up @@ -224,6 +251,14 @@ extension CurrentUserUpdater {
}
}

func deleteAllAttachmentLocalDownloads() async throws {
try await withCheckedThrowingContinuation { continuation in
deleteAllAttachmentLocalDownloads() { error in

Check warning on line 256 in Sources/StreamChat/Workers/CurrentUserUpdater.swift

View workflow job for this annotation

GitHub Actions / Test LLC (Debug)

Empty Parentheses with Trailing Closure Violation: When using trailing closures, empty parentheses should be avoided after the method call (empty_parentheses_with_trailing_closure)
continuation.resume(with: error)
}
}
}

func fetchDevices(currentUserId: UserId) async throws -> [Device] {
try await withCheckedThrowingContinuation { continuation in
fetchDevices(currentUserId: currentUserId) { result in
Expand Down
29 changes: 29 additions & 0 deletions Sources/StreamChat/Workers/MessageUpdater.swift
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,8 @@ class MessageUpdater: Worker {
}
}

// MARK: - Attachments

static let minSignificantDownloadingProgressChange: Double = 0.01

func downloadAttachment(
Expand Down Expand Up @@ -613,6 +615,23 @@ class MessageUpdater: Worker {
}
}

func deleteLocalAttachmentDownload(for attachmentId: AttachmentId, completion: @escaping (Error?) -> Void) {
database.write({ session in
let dto = session.attachment(id: attachmentId)
guard let attachment = dto?.asAnyModel() else {
throw ClientError.AttachmentDoesNotExist(id: attachmentId)
}
guard attachment.downloadingState?.state == .downloaded else { return }
guard let localURL = attachment.downloadingState?.localFileURL else { return }
guard FileManager.default.fileExists(atPath: localURL.path) else { return }
try FileManager.default.removeItem(at: localURL)
dto?.localState = nil
dto?.localRelativePath = nil
dto?.localState = nil
dto?.localURL = nil
}, completion: completion)
}

private func fileAttachment(with attachmentId: AttachmentId, completion: @escaping (Result<ChatMessageFileAttachment, Error>) -> Void) {
database.read({ session in
guard let attachment = session.attachment(id: attachmentId)?.asAnyModel() else {
Expand Down Expand Up @@ -666,6 +685,8 @@ class MessageUpdater: Worker {
attachmentDTO.localState = .pendingUpload
}, completion: completion)
}

// MARK: -

/// Updates local state of the message with provided `messageId` to be enqueued by message sender background worker.
/// - Parameters:
Expand Down Expand Up @@ -1011,6 +1032,14 @@ extension MessageUpdater {
}
}

func deleteLocalAttachmentDownload(for attachmentId: AttachmentId) async throws {
try await withCheckedThrowingContinuation { continuation in
deleteLocalAttachmentDownload(for: attachmentId) { error in
continuation.resume(with: error)
}
}
}

func deleteMessage(messageId: MessageId, hard: Bool) async throws {
try await withCheckedThrowingContinuation { continuation in
deleteMessage(messageId: messageId, hard: hard) { error in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -998,7 +998,7 @@ open class ChatMessageListVC: _ViewController,
let chat = client.makeChat(for: attachment.id.cid)
_Concurrency.Task {
do {
try await chat.downloadAttachment(attachment.id)
try await chat.downloadAttachment(with: attachment.id)
} catch {
log.debug("Downloaded attachment for id \(attachment.id)")
}
Expand Down

0 comments on commit e6564af

Please sign in to comment.