From 67089e5a891f73ad039dfc163c069b03a2ac6cdc Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 6 Feb 2026 11:50:53 +0100 Subject: [PATCH 1/4] code Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+Search.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index 5aa7a4ed..431493a7 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -309,6 +309,7 @@ public class NKSearchProvider: NSObject { public let id, name: String public let order: Int + // Initialize from JSON init?(json: JSON) { guard let id = json["id"].string, let name = json["name"].string, @@ -319,6 +320,14 @@ public class NKSearchProvider: NSObject { self.order = order } + // Classic initializer + public init(id: String, name: String, order: Int) { + self.id = id + self.name = name + self.order = order + super.init() + } + static func factory(jsonArray: JSON) -> [NKSearchProvider]? { guard let allProvider = jsonArray.array else { return nil } return allProvider.compactMap(NKSearchProvider.init) From e329efa33f617c3f2a516d30f1e759338bd59d38 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 6 Feb 2026 14:46:49 +0100 Subject: [PATCH 2/4] code Signed-off-by: Marino Faggiana --- .../NextcloudKit/NextcloudKit+Search.swift | 237 +++++------------- 1 file changed, 58 insertions(+), 179 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index 431493a7..e1c4b9c9 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -8,129 +8,56 @@ import Alamofire import SwiftyJSON public extension NextcloudKit { - /// Available NC >= 20 /// Performs a unified search using multiple providers and returns results asynchronously. /// /// - Parameters: - /// - term: The search term to query. /// - timeout: The individual request timeout per provider. - /// - timeoutProvider: The maximum time allowed for each provider before being cancelled. /// - account: The Nextcloud account performing the search. /// - options: Optional configuration for the request (headers, queue, etc.). /// - filter: A closure to filter which `NKSearchProvider` are enabled. - /// - request: Callback to access and inspect the underlying `DataRequest?`. /// - taskHandler: Callback triggered when a `URLSessionTask` is created. - /// - providers: Callback providing the list of providers that will be queried. - /// - update: Called for every result update from a provider. - /// - completion: Called when all providers are finished, returns the response and status. - func unifiedSearch(term: String, - timeout: TimeInterval = 30, - timeoutProvider: TimeInterval = 60, - account: String, - options: NKRequestOptions = NKRequestOptions(), - filter: @escaping (NKSearchProvider) -> Bool = { _ in true }, - request: @escaping (DataRequest?) -> Void, - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - providers: @escaping (_ account: String, _ searchProviders: [NKSearchProvider]?) -> Void, - update: @escaping (_ account: String, _ searchResult: NKSearchResult?, _ provider: NKSearchProvider, _ error: NKError) -> Void, - completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { + /// + /// - Returns: NKSearchProvider, NKError + func unifiedSearchProviders(timeout: TimeInterval = 30, + account: String, + options: NKRequestOptions = NKRequestOptions(), + filter: @escaping (NKSearchProvider) -> Bool = { _ in true }, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> (providers: [NKSearchProvider]?, error: NKError) { let endpoint = "ocs/v2.php/search/providers" guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, .urlError) } + return (nil, .urlError) } - let requestUnifiedSearch = nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, 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 .success(let jsonData): - let json = JSON(jsonData) - let providerData = json["ocs"]["data"] - guard let allProvider = NKSearchProvider.factory(jsonArray: providerData) else { - return completion(account, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) - } - providers(account, allProvider) - - let filteredProviders = allProvider.filter(filter) - let group = DispatchGroup() + let request = nkSession.sessionData + .request(url, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) + .validate(statusCode: 200..<300) + .onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + } + let response = await request.serializingData().response - for provider in filteredProviders { - group.enter() - let requestSearchProvider = self.searchProvider(provider.id, term: term, timeout: timeoutProvider, account: account, options: options) { account, partial, _, error in - update(account, partial, provider, error) - group.leave() - } - request(requestSearchProvider) - } + switch response.result { + case .success(let jsonData): + let json = JSON(jsonData) + let providerData = json["ocs"]["data"] + let providers = NKSearchProvider.factory(jsonArray: providerData)?.filter(filter) - group.notify(queue: options.queue) { - completion(account, response, .success) - } - case .failure(let error): - let error = NKError(error: error, afResponse: response, responseData: response.data) - return completion(account, response, error) - } - } - request(requestUnifiedSearch) - } + return(providers, .success) + case .failure(let error): + let nkError = NKError(error: error, afResponse: response, responseData: response.data) - /// Asynchronously performs a unified search and returns the final search response. - /// - /// - Parameters: - /// - term: The string to search for. - /// - timeout: Per-provider timeout in seconds. - /// - timeoutProvider: Overall timeout for a provider. - /// - account: The account used to authenticate the request. - /// - options: Optional parameters for the search. - /// - filter: Closure to filter the search providers. - /// - request: Callback with the underlying `DataRequest?`. - /// - taskHandler: Monitors the task creation. - /// - providers: Callback that reports which providers are used. - /// - update: Callback triggered as results come in from providers. - /// - Returns: Final completion with account, raw response data, and NKError. - func unifiedSearchAsync(term: String, - timeout: TimeInterval = 30, - timeoutProvider: TimeInterval = 60, - account: String, - options: NKRequestOptions = NKRequestOptions(), - filter: @escaping (NKSearchProvider) -> Bool = { _ in true }, - request: @escaping (DataRequest?) -> Void, - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - providers: @escaping (_ account: String, _ searchProviders: [NKSearchProvider]?) -> Void, - update: @escaping (_ account: String, _ searchResult: NKSearchResult?, _ provider: NKSearchProvider, _ error: NKError) -> Void - ) async -> ( - account: String, - responseData: AFDataResponse?, - error: NKError - ) { - await withCheckedContinuation { continuation in - unifiedSearch(term: term, - timeout: timeout, - timeoutProvider: timeoutProvider, - account: account, - options: options, - filter: filter, - request: request, - taskHandler: taskHandler, - providers: providers, - update: update) { account, responseData, error in - continuation.resume(returning: ( - account: account, - responseData: responseData, - error: error - )) - } + return (nil, nkError) } } - /// Available NC >= 20 /// Performs a search using a specified provider with pagination and timeout support. /// /// - Parameters: - /// - id: The identifier of the search provider to use. + /// - idProvider: The identifier of the search provider to use. /// - term: The search term. /// - limit: Optional maximum number of results to return. /// - cursor: Optional pagination cursor for subsequent requests. @@ -138,25 +65,23 @@ public extension NextcloudKit { /// - account: The Nextcloud account performing the search. /// - options: Optional request configuration such as headers and queue. /// - taskHandler: Callback to observe the underlying URLSessionTask. - /// - completion: Completion handler returning the account, search results, raw response, and NKError. /// - /// - Returns: The underlying DataRequest object if the request was started, otherwise nil. - func searchProvider(_ id: String, - term: String, - limit: Int? = nil, - cursor: Int? = nil, - timeout: TimeInterval = 60, - account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, NKSearchResult?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) -> DataRequest? { + /// - Returns: NKSearchResult, NKError + func unifiedSearch(idProvider: String, + term: String, + limit: Int? = nil, + cursor: Int? = nil, + timeout: TimeInterval = 60, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }) + async -> (searchResult: NKSearchResult?, error: NKError) { guard let term = term.urlEncoded, let nkSession = nkCommonInstance.nksessions.session(forAccount: account), let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - completion(account, nil, nil, .urlError) - return nil + return(nil, .urlError) } - var endpoint = "ocs/v2.php/search/providers/\(id)/search?term=\(term)" + var endpoint = "ocs/v2.php/search/providers/\(idProvider)/search?term=\(term)" if let limit = limit { endpoint += "&limit=\(limit)" } @@ -165,8 +90,7 @@ public extension NextcloudKit { } guard let url = self.nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint) else { - completion(account, nil, nil, .urlError) - return nil + return(nil, .urlError) } var urlRequest: URLRequest @@ -174,74 +98,29 @@ public extension NextcloudKit { try urlRequest = URLRequest(url: url, method: .get, headers: headers) urlRequest.timeoutInterval = timeout } catch { - completion(account, nil, nil, NKError(error: error)) - return nil + return(nil, NKError(error: error)) } - let requestSearchProvider = 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 .success(let jsonData): - let json = JSON(jsonData) - let searchData = json["ocs"]["data"] - guard let searchResult = NKSearchResult(json: searchData, id: id) else { - return completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) - } - completion(account, searchResult, response, .success) - case .failure(let error): - let error = NKError(error: error, afResponse: response, responseData: response.data) - return completion(account, nil, response, error) + let request = nkSession.sessionData + .request(urlRequest, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)) + .validate(statusCode: 200..<300) + .onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) } - } + let response = await request.serializingData().response - return requestSearchProvider - } + switch response.result { + case .success(let jsonData): + let json = JSON(jsonData) + let searchData = json["ocs"]["data"] + let searchResult = NKSearchResult(json: searchData, id: idProvider) - /// Asynchronously performs a search request using the specified provider. - /// - /// - Parameters: - /// - id: The identifier of the search provider to use. - /// - term: The search query string. - /// - limit: Optional limit for number of results. - /// - cursor: Optional pagination cursor. - /// - timeout: The timeout for the request. - /// - account: The Nextcloud account performing the request. - /// - options: Optional configuration options for the request. - /// - taskHandler: Callback to observe the created task. - /// - /// - Returns: A tuple containing the account, search result, response data, and error. - func searchProviderAsync(_ id: String, - term: String, - limit: Int? = nil, - cursor: Int? = nil, - timeout: TimeInterval = 60, - account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } - ) async -> ( - account: String, - searchResult: NKSearchResult?, - responseData: AFDataResponse?, - error: NKError - ) { - await withCheckedContinuation { continuation in - _ = searchProvider(id, - term: term, - limit: limit, - cursor: cursor, - timeout: timeout, - account: account, - options: options, - taskHandler: taskHandler) { account, result, responseData, error in - continuation.resume(returning: ( - account: account, - searchResult: result, - responseData: responseData, - error: error - )) - } + return (searchResult, .success) + case .failure(let error): + let nkError = NKError(error: error, afResponse: response, responseData: response.data) + + return (nil, nkError) } } } From 58090052d175e8c27fb365b83cb957235e062c94 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 6 Feb 2026 14:52:53 +0100 Subject: [PATCH 3/4] code Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+Search.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index e1c4b9c9..bfc0bcee 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -57,7 +57,7 @@ public extension NextcloudKit { /// Performs a search using a specified provider with pagination and timeout support. /// /// - Parameters: - /// - idProvider: The identifier of the search provider to use. + /// - providerId: The identifier of the search provider to use. /// - term: The search term. /// - limit: Optional maximum number of results to return. /// - cursor: Optional pagination cursor for subsequent requests. @@ -67,7 +67,7 @@ public extension NextcloudKit { /// - taskHandler: Callback to observe the underlying URLSessionTask. /// /// - Returns: NKSearchResult, NKError - func unifiedSearch(idProvider: String, + func unifiedSearch(providerId: String, term: String, limit: Int? = nil, cursor: Int? = nil, @@ -81,7 +81,7 @@ public extension NextcloudKit { let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { return(nil, .urlError) } - var endpoint = "ocs/v2.php/search/providers/\(idProvider)/search?term=\(term)" + var endpoint = "ocs/v2.php/search/providers/\(providerId)/search?term=\(term)" if let limit = limit { endpoint += "&limit=\(limit)" } @@ -114,7 +114,7 @@ public extension NextcloudKit { case .success(let jsonData): let json = JSON(jsonData) let searchData = json["ocs"]["data"] - let searchResult = NKSearchResult(json: searchData, id: idProvider) + let searchResult = NKSearchResult(json: searchData, id: providerId) return (searchResult, .success) case .failure(let error): From 04415ec488fa99fa05b5e381f709fe8b5f42f918 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sun, 8 Feb 2026 10:35:29 +0100 Subject: [PATCH 4/4] code Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NextcloudKit+Search.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/NextcloudKit/NextcloudKit+Search.swift b/Sources/NextcloudKit/NextcloudKit+Search.swift index bfc0bcee..c8f5124a 100644 --- a/Sources/NextcloudKit/NextcloudKit+Search.swift +++ b/Sources/NextcloudKit/NextcloudKit+Search.swift @@ -1,6 +1,5 @@ // SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2022 Henrik Storch -// SPDX-FileCopyrightText: 2023 Marino Faggiana +// SPDX-FileCopyrightText: 2023/2026 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later import Foundation