From 34989ff33fe881a7c580cd787a96a2fe4968ad74 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 1 Apr 2026 17:13:07 +0200 Subject: [PATCH 1/2] WIP Signed-off-by: Milen Pivchev --- .../NextcloudKit/Models/NKDataFileXML.swift | 12 + Sources/NextcloudKit/Models/NKTag.swift | 17 + Sources/NextcloudKit/NextcloudKit+Tags.swift | 300 ++++++++++++++++++ 3 files changed, 329 insertions(+) create mode 100644 Sources/NextcloudKit/Models/NKTag.swift create mode 100644 Sources/NextcloudKit/NextcloudKit+Tags.swift diff --git a/Sources/NextcloudKit/Models/NKDataFileXML.swift b/Sources/NextcloudKit/Models/NKDataFileXML.swift index cdb27649..c0d11bb9 100644 --- a/Sources/NextcloudKit/Models/NKDataFileXML.swift +++ b/Sources/NextcloudKit/Models/NKDataFileXML.swift @@ -89,6 +89,18 @@ public class NKDataFileXML: NSObject { """ + let requestBodySystemTags = + """ + + + + + + + + + """ + func getRequestBodyFileListingFavorites(createProperties: [NKProperties]?, removeProperties: [NKProperties] = []) -> String { let request = """ diff --git a/Sources/NextcloudKit/Models/NKTag.swift b/Sources/NextcloudKit/Models/NKTag.swift new file mode 100644 index 00000000..94ab5ae0 --- /dev/null +++ b/Sources/NextcloudKit/Models/NKTag.swift @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +public struct NKTag: Sendable, Equatable, Hashable { + public let id: String + public let name: String + public let color: String? + + public init(id: String, name: String, color: String?) { + self.id = id + self.name = name + self.color = color + } +} diff --git a/Sources/NextcloudKit/NextcloudKit+Tags.swift b/Sources/NextcloudKit/NextcloudKit+Tags.swift new file mode 100644 index 00000000..a29432af --- /dev/null +++ b/Sources/NextcloudKit/NextcloudKit+Tags.swift @@ -0,0 +1,300 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import Alamofire +import SwiftyJSON +import SwiftyXMLParser + +public extension NextcloudKit { + private var systemTagsPath: String { "/remote.php/dav/systemtags/" } + private var systemTagRelationsFilesPath: String { "/remote.php/dav/systemtags-relations/files/" } + + /// Returns the list of tags available for the account. + /// + /// - Parameters: + /// - account: The account performing the request. + /// - options: Optional request options. + /// - taskHandler: Callback for the underlying URL session task. + /// - completion: Completion handler returning account, tags, raw response and error. + func getTags(account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ tags: [NKTag]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), + var headers = nkCommonInstance.getStandardHeaders(account: account, options: options, accept: "application/xml") else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } + + let endpoint = nkSession.urlBase + systemTagsPath + guard let url = endpoint.encodedToUrl else { + return options.queue.async { completion(account, nil, nil, .urlError) } + } + + let method = HTTPMethod(rawValue: "PROPFIND") + headers.update(name: "Depth", value: "1") + var urlRequest: URLRequest + do { + try urlRequest = URLRequest(url: url, method: method, headers: headers) + urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodySystemTags.data(using: .utf8) + urlRequest.timeoutInterval = options.timeout + } catch { + return options.queue.async { completion(account, nil, nil, NKError(error: error)) } + } + + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) + .validate(statusCode: 200..<300) + .onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + } + .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { completion(account, nil, response, error) } + case .success: + guard let xmlData = response.data else { + return options.queue.async { completion(account, nil, response, .invalidData) } + } + let tags = self.convertSystemTags(xmlData: xmlData) + options.queue.async { completion(account, tags, response, .success) } + } + } + } + + /// Async wrapper around ``getTags(account:options:taskHandler:completion:)``. + func getTagsAsync(account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> ( + account: String, + tags: [NKTag]?, + responseData: AFDataResponse?, + error: NKError + ) { + await withCheckedContinuation { continuation in + getTags(account: account, options: options, taskHandler: taskHandler) { account, tags, responseData, error in + continuation.resume(returning: ( + account: account, + tags: tags, + responseData: responseData, + error: error + )) + } + } + } + + /// Creates a new tag. + /// + /// - Parameters: + /// - name: Tag display name. + /// - account: Account performing the request. + /// - options: Optional request options. + /// - taskHandler: Callback for the underlying URL session task. + /// - completion: Completion handler returning account, raw response and error. + func createTag(name: String, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options, contentType: "application/json", accept: "application/json"), + let url = (nkSession.urlBase + systemTagsPath).encodedToUrl else { + return options.queue.async { completion(account, nil, .urlError) } + } + + var urlRequest: URLRequest + do { + try urlRequest = URLRequest(url: url, method: .post, headers: headers) + urlRequest.timeoutInterval = options.timeout + let payload = ["name": name] + urlRequest.httpBody = try JSONSerialization.data(withJSONObject: payload) + } catch { + return options.queue.async { completion(account, nil, NKError(error: error)) } + } + + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) + .validate(statusCode: 200..<300) + .onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + } + .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + let result = self.evaluateResponse(response) + options.queue.async { completion(account, response, result) } + } + } + + /// Async wrapper around ``createTag(name:account:options:taskHandler:completion:)``. + func createTagAsync(name: String, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> ( + account: String, + responseData: AFDataResponse?, + error: NKError + ) { + await withCheckedContinuation { continuation in + createTag(name: name, account: account, options: options, taskHandler: taskHandler) { account, responseData, error in + continuation.resume(returning: ( + account: account, + responseData: responseData, + error: error + )) + } + } + } + + /// Assigns a tag to a file by file id. + /// + /// - Parameters: + /// - tagId: The system tag id. + /// - fileId: The numeric file id. + /// - account: Account performing the request. + /// - options: Optional request options. + /// - taskHandler: Callback for the underlying URL session task. + /// - completion: Completion handler returning account, raw response and error. + func addTagToFile(tagId: String, + fileId: String, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options, contentType: "application/json", accept: "application/json"), + let url = (nkSession.urlBase + systemTagRelationsFilesPath + fileId + "/" + tagId).encodedToUrl else { + return options.queue.async { completion(account, nil, .urlError) } + } + + var urlRequest: URLRequest + do { + try urlRequest = URLRequest(url: url, method: .put, headers: headers) + urlRequest.timeoutInterval = options.timeout + urlRequest.httpBody = Data() + } catch { + return options.queue.async { completion(account, nil, NKError(error: error)) } + } + + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) + .validate(statusCode: 200..<300) + .onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + } + .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + let result = self.evaluateResponse(response) + options.queue.async { completion(account, response, result) } + } + } + + /// Async wrapper around ``addTagToFile(tagId:fileId:account:options:taskHandler:completion:)``. + func addTagToFileAsync(tagId: String, + fileId: String, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> ( + account: String, + responseData: AFDataResponse?, + error: NKError + ) { + await withCheckedContinuation { continuation in + addTagToFile(tagId: tagId, fileId: fileId, account: account, options: options, taskHandler: taskHandler) { account, responseData, error in + continuation.resume(returning: ( + account: account, + responseData: responseData, + error: error + )) + } + } + } + + /// Removes a tag assignment from a file. + /// + /// - Parameters: + /// - tagId: The system tag id. + /// - fileId: The numeric file id. + /// - account: Account performing the request. + /// - options: Optional request options. + /// - taskHandler: Callback for the underlying URL session task. + /// - completion: Completion handler returning account, raw response and error. + func removeTagFromFile(tagId: String, + fileId: String, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options, contentType: "application/json", accept: "application/json"), + let url = (nkSession.urlBase + systemTagRelationsFilesPath + fileId + "/" + tagId).encodedToUrl else { + return options.queue.async { completion(account, nil, .urlError) } + } + + var urlRequest: URLRequest + do { + try urlRequest = URLRequest(url: url, method: .delete, headers: headers) + urlRequest.timeoutInterval = options.timeout + } catch { + return options.queue.async { completion(account, nil, NKError(error: error)) } + } + + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) + .validate(statusCode: 200..<300) + .onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + } + .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + let result = self.evaluateResponse(response) + options.queue.async { completion(account, response, result) } + } + } + + /// Async wrapper around ``removeTagFromFile(tagId:fileId:account:options:taskHandler:completion:)``. + func removeTagFromFileAsync(tagId: String, + fileId: String, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> ( + account: String, + responseData: AFDataResponse?, + error: NKError + ) { + await withCheckedContinuation { continuation in + removeTagFromFile(tagId: tagId, fileId: fileId, account: account, options: options, taskHandler: taskHandler) { account, responseData, error in + continuation.resume(returning: ( + account: account, + responseData: responseData, + error: error + )) + } + } + } + + private func convertSystemTags(xmlData: Data) -> [NKTag] { + let xml = XML.parse(xmlData) + let responses = xml["d:multistatus", "d:response"] + var tags: [NKTag] = [] + + for response in responses { + let propstat = response["d:propstat"][0] + guard let id = propstat["d:prop", "oc:id"].text, + let name = propstat["d:prop", "oc:display-name"].text else { + continue + } + + var color: String? + if let colorHex = propstat["d:prop", "nc:color"].text, !colorHex.isEmpty { + color = colorHex.hasPrefix("#") ? colorHex : "#\(colorHex)" + } + + tags.append(NKTag(id: id, name: name, color: color)) + } + + return tags + } +} From d7882e25d75735afc1347e043a025b15eee1331c Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Fri, 10 Apr 2026 18:36:18 +0200 Subject: [PATCH 2/2] WIP Signed-off-by: Milen Pivchev --- .../NextcloudKit/Models/NKDataFileXML.swift | 7 +- Sources/NextcloudKit/Models/NKFile.swift | 4 +- Sources/NextcloudKit/Models/NKTag.swift | 45 +++ Sources/NextcloudKit/NextcloudKit+Tags.swift | 326 ++++++++---------- 4 files changed, 200 insertions(+), 182 deletions(-) diff --git a/Sources/NextcloudKit/Models/NKDataFileXML.swift b/Sources/NextcloudKit/Models/NKDataFileXML.swift index c0d11bb9..0d76e267 100644 --- a/Sources/NextcloudKit/Models/NKDataFileXML.swift +++ b/Sources/NextcloudKit/Models/NKDataFileXML.swift @@ -464,11 +464,8 @@ public class NKDataFileXML: NSObject { file.lockTimeOut = file.lockTime?.addingTimeInterval(TimeInterval(lockTimeOut)) } - let tagsElements = propstat["d:prop", "nc:system-tags"] - for element in tagsElements["nc:system-tag"] { - guard let tag = element.text else { continue } - file.tags.append(tag) - } + let tags: [NKTag] = NKTag.parse(systemTagElements: propstat["d:prop", "nc:system-tags", "nc:system-tag"]) + file.tags.append(contentsOf: tags) // NC27 ----- if let latitude = propstat["d:prop", "nc:file-metadata-gps", "latitude"].double { diff --git a/Sources/NextcloudKit/Models/NKFile.swift b/Sources/NextcloudKit/Models/NKFile.swift index f048edac..53335296 100644 --- a/Sources/NextcloudKit/Models/NKFile.swift +++ b/Sources/NextcloudKit/Models/NKFile.swift @@ -56,7 +56,7 @@ public struct NKFile: Sendable { public var shareType: [Int] public var size: Int64 public var serverUrl: String - public var tags: [String] + public var tags: [NKTag] public var trashbinFileName: String public var trashbinOriginalLocation: String public var trashbinDeletionTime: Date @@ -128,7 +128,7 @@ public struct NKFile: Sendable { shareType: [Int] = [], size: Int64 = 0, serverUrl: String = "", - tags: [String] = [], + tags: [NKTag] = [], trashbinFileName: String = "", trashbinOriginalLocation: String = "", trashbinDeletionTime: Date = Date(), diff --git a/Sources/NextcloudKit/Models/NKTag.swift b/Sources/NextcloudKit/Models/NKTag.swift index 94ab5ae0..77a9bacb 100644 --- a/Sources/NextcloudKit/Models/NKTag.swift +++ b/Sources/NextcloudKit/Models/NKTag.swift @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later import Foundation +import SwiftyXMLParser public struct NKTag: Sendable, Equatable, Hashable { public let id: String @@ -14,4 +15,48 @@ public struct NKTag: Sendable, Equatable, Hashable { self.name = name self.color = color } + + static func parse(xmlData: Data) -> [NKTag] { + let xml = XML.parse(xmlData) + let responses = xml["d:multistatus", "d:response"] + var tags: [NKTag] = [] + + for response in responses { + let propstat = response["d:propstat"][0] + guard let id = propstat["d:prop", "oc:id"].text, + let name = propstat["d:prop", "oc:display-name"].text else { + continue + } + + let color = normalizedColor(propstat["d:prop", "nc:color"].text) + + tags.append(NKTag(id: id, name: name, color: color)) + } + + return tags + } + + static func parse(systemTagElements: XML.Accessor) -> [NKTag] { + var tags: [NKTag] = [] + + for element in systemTagElements { + guard let name = element.text?.trimmingCharacters(in: .whitespacesAndNewlines), + !name.isEmpty else { + continue + } + + let id = element.attributes["oc:id"] ?? "" + let color = normalizedColor(element.attributes["nc:color"]) + tags.append(NKTag(id: id, name: name, color: color)) + } + + return tags + } + + private static func normalizedColor(_ rawValue: String?) -> String? { + guard let rawValue, !rawValue.isEmpty else { + return nil + } + return rawValue.hasPrefix("#") ? rawValue : "#\(rawValue)" + } } diff --git a/Sources/NextcloudKit/NextcloudKit+Tags.swift b/Sources/NextcloudKit/NextcloudKit+Tags.swift index a29432af..876945e9 100644 --- a/Sources/NextcloudKit/NextcloudKit+Tags.swift +++ b/Sources/NextcloudKit/NextcloudKit+Tags.swift @@ -4,8 +4,6 @@ import Foundation import Alamofire -import SwiftyJSON -import SwiftyXMLParser public extension NextcloudKit { private var systemTagsPath: String { "/remote.php/dav/systemtags/" } @@ -17,19 +15,33 @@ public extension NextcloudKit { /// - account: The account performing the request. /// - options: Optional request options. /// - taskHandler: Callback for the underlying URL session task. - /// - completion: Completion handler returning account, tags, raw response and error. func getTags(account: String, options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ tags: [NKTag]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> ( + account: String, + tags: [NKTag]?, + responseData: AFDataResponse?, + error: NKError + ) { guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), var headers = nkCommonInstance.getStandardHeaders(account: account, options: options, accept: "application/xml") else { - return options.queue.async { completion(account, nil, nil, .urlError) } + return ( + account: account, + tags: nil, + responseData: nil, + error: .urlError + ) } let endpoint = nkSession.urlBase + systemTagsPath guard let url = endpoint.encodedToUrl else { - return options.queue.async { completion(account, nil, nil, .urlError) } + return ( + account: account, + tags: nil, + responseData: nil, + error: .urlError + ) } let method = HTTPMethod(rawValue: "PROPFIND") @@ -40,49 +52,49 @@ public extension NextcloudKit { urlRequest.httpBody = NKDataFileXML(nkCommonInstance: self.nkCommonInstance).requestBodySystemTags.data(using: .utf8) urlRequest.timeoutInterval = options.timeout } catch { - return options.queue.async { completion(account, nil, nil, NKError(error: error)) } + return ( + account: account, + tags: nil, + responseData: nil, + error: NKError(error: error) + ) } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) - .validate(statusCode: 200..<300) - .onURLSessionTaskCreation { task in - task.taskDescription = options.taskDescription - taskHandler(task) - } - .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in - switch response.result { - case .failure(let error): - let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, response, error) } - case .success: - guard let xmlData = response.data else { - return options.queue.async { completion(account, nil, response, .invalidData) } + return await withCheckedContinuation { continuation in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) + .validate(statusCode: 200..<300) + .onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + } + .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + continuation.resume(returning: ( + account: account, + tags: nil, + responseData: response, + error: error + )) + case .success: + guard let xmlData = response.data else { + return continuation.resume(returning: ( + account: account, + tags: nil, + responseData: response, + error: .invalidData + )) + } + let tags = NKTag.parse(xmlData: xmlData) + continuation.resume(returning: ( + account: account, + tags: tags, + responseData: response, + error: .success + )) } - let tags = self.convertSystemTags(xmlData: xmlData) - options.queue.async { completion(account, tags, response, .success) } } - } - } - - /// Async wrapper around ``getTags(account:options:taskHandler:completion:)``. - func getTagsAsync(account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } - ) async -> ( - account: String, - tags: [NKTag]?, - responseData: AFDataResponse?, - error: NKError - ) { - await withCheckedContinuation { continuation in - getTags(account: account, options: options, taskHandler: taskHandler) { account, tags, responseData, error in - continuation.resume(returning: ( - account: account, - tags: tags, - responseData: responseData, - error: error - )) - } } } @@ -93,16 +105,23 @@ public extension NextcloudKit { /// - account: Account performing the request. /// - options: Optional request options. /// - taskHandler: Callback for the underlying URL session task. - /// - completion: Completion handler returning account, raw response and error. func createTag(name: String, account: String, options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> ( + account: String, + responseData: AFDataResponse?, + error: NKError + ) { guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options, contentType: "application/json", accept: "application/json"), let url = (nkSession.urlBase + systemTagsPath).encodedToUrl else { - return options.queue.async { completion(account, nil, .urlError) } + return ( + account: account, + responseData: nil, + error: .urlError + ) } var urlRequest: URLRequest @@ -112,39 +131,28 @@ public extension NextcloudKit { let payload = ["name": name] urlRequest.httpBody = try JSONSerialization.data(withJSONObject: payload) } catch { - return options.queue.async { completion(account, nil, NKError(error: error)) } + return ( + account: account, + responseData: nil, + error: NKError(error: error) + ) } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) - .validate(statusCode: 200..<300) - .onURLSessionTaskCreation { task in - task.taskDescription = options.taskDescription - taskHandler(task) - } - .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in - let result = self.evaluateResponse(response) - options.queue.async { completion(account, response, result) } - } - } - - /// Async wrapper around ``createTag(name:account:options:taskHandler:completion:)``. - func createTagAsync(name: String, - account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } - ) async -> ( - account: String, - responseData: AFDataResponse?, - error: NKError - ) { - await withCheckedContinuation { continuation in - createTag(name: name, account: account, options: options, taskHandler: taskHandler) { account, responseData, error in - continuation.resume(returning: ( - account: account, - responseData: responseData, - error: error - )) - } + return await withCheckedContinuation { continuation in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) + .validate(statusCode: 200..<300) + .onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + } + .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + let result = self.evaluateResponse(response) + continuation.resume(returning: ( + account: account, + responseData: response, + error: result + )) + } } } @@ -156,17 +164,24 @@ public extension NextcloudKit { /// - account: Account performing the request. /// - options: Optional request options. /// - taskHandler: Callback for the underlying URL session task. - /// - completion: Completion handler returning account, raw response and error. func addTagToFile(tagId: String, fileId: String, account: String, options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> ( + account: String, + responseData: AFDataResponse?, + error: NKError + ) { guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options, contentType: "application/json", accept: "application/json"), let url = (nkSession.urlBase + systemTagRelationsFilesPath + fileId + "/" + tagId).encodedToUrl else { - return options.queue.async { completion(account, nil, .urlError) } + return ( + account: account, + responseData: nil, + error: .urlError + ) } var urlRequest: URLRequest @@ -175,40 +190,28 @@ public extension NextcloudKit { urlRequest.timeoutInterval = options.timeout urlRequest.httpBody = Data() } catch { - return options.queue.async { completion(account, nil, NKError(error: error)) } + return ( + account: account, + responseData: nil, + error: NKError(error: error) + ) } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) - .validate(statusCode: 200..<300) - .onURLSessionTaskCreation { task in - task.taskDescription = options.taskDescription - taskHandler(task) - } - .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in - let result = self.evaluateResponse(response) - options.queue.async { completion(account, response, result) } - } - } - - /// Async wrapper around ``addTagToFile(tagId:fileId:account:options:taskHandler:completion:)``. - func addTagToFileAsync(tagId: String, - fileId: String, - account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } - ) async -> ( - account: String, - responseData: AFDataResponse?, - error: NKError - ) { - await withCheckedContinuation { continuation in - addTagToFile(tagId: tagId, fileId: fileId, account: account, options: options, taskHandler: taskHandler) { account, responseData, error in - continuation.resume(returning: ( - account: account, - responseData: responseData, - error: error - )) - } + return await withCheckedContinuation { continuation in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) + .validate(statusCode: 200..<300) + .onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + } + .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + let result = self.evaluateResponse(response) + continuation.resume(returning: ( + account: account, + responseData: response, + error: result + )) + } } } @@ -220,17 +223,24 @@ public extension NextcloudKit { /// - account: Account performing the request. /// - options: Optional request options. /// - taskHandler: Callback for the underlying URL session task. - /// - completion: Completion handler returning account, raw response and error. func removeTagFromFile(tagId: String, fileId: String, account: String, options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> ( + account: String, + responseData: AFDataResponse?, + error: NKError + ) { guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options, contentType: "application/json", accept: "application/json"), let url = (nkSession.urlBase + systemTagRelationsFilesPath + fileId + "/" + tagId).encodedToUrl else { - return options.queue.async { completion(account, nil, .urlError) } + return ( + account: account, + responseData: nil, + error: .urlError + ) } var urlRequest: URLRequest @@ -238,63 +248,29 @@ public extension NextcloudKit { try urlRequest = URLRequest(url: url, method: .delete, headers: headers) urlRequest.timeoutInterval = options.timeout } catch { - return options.queue.async { completion(account, nil, NKError(error: error)) } + return ( + account: account, + responseData: nil, + error: NKError(error: error) + ) } - nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) - .validate(statusCode: 200..<300) - .onURLSessionTaskCreation { task in - task.taskDescription = options.taskDescription - taskHandler(task) - } - .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in - let result = self.evaluateResponse(response) - options.queue.async { completion(account, response, result) } - } - } - - /// Async wrapper around ``removeTagFromFile(tagId:fileId:account:options:taskHandler:completion:)``. - func removeTagFromFileAsync(tagId: String, - fileId: String, - account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } - ) async -> ( - account: String, - responseData: AFDataResponse?, - error: NKError - ) { - await withCheckedContinuation { continuation in - removeTagFromFile(tagId: tagId, fileId: fileId, account: account, options: options, taskHandler: taskHandler) { account, responseData, error in - continuation.resume(returning: ( - account: account, - responseData: responseData, - error: error - )) - } + return await withCheckedContinuation { continuation in + nkSession.sessionData.request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) + .validate(statusCode: 200..<300) + .onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + } + .responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + let result = self.evaluateResponse(response) + continuation.resume(returning: ( + account: account, + responseData: response, + error: result + )) + } } } - private func convertSystemTags(xmlData: Data) -> [NKTag] { - let xml = XML.parse(xmlData) - let responses = xml["d:multistatus", "d:response"] - var tags: [NKTag] = [] - - for response in responses { - let propstat = response["d:propstat"][0] - guard let id = propstat["d:prop", "oc:id"].text, - let name = propstat["d:prop", "oc:display-name"].text else { - continue - } - - var color: String? - if let colorHex = propstat["d:prop", "nc:color"].text, !colorHex.isEmpty { - color = colorHex.hasPrefix("#") ? colorHex : "#\(colorHex)" - } - - tags.append(NKTag(id: id, name: name, color: color)) - } - - return tags - } }